From 79527311236c144f2f003a10a20d973f6223d2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Barbeau?= Date: Thu, 19 Sep 2019 16:13:56 -0400 Subject: [PATCH] fix(import-export): better error handling --- demo/src/app/app.component.html | 1 + demo/src/app/app.module.ts | 2 + .../import-export-routing.module.ts | 15 +++++ .../import-export.component.html | 20 +++++++ .../import-export.component.scss | 4 ++ .../import-export/import-export.component.ts | 43 ++++++++++++++ .../geo/import-export/import-export.module.ts | 22 +++++++ demo/src/environments/environment.ts | 3 + .../lib/import-export/import-export.module.ts | 3 +- .../import-export.component.html | 12 +++- .../import-export.component.scss | 5 ++ .../import-export/import-export.component.ts | 38 ++++++++---- .../lib/import-export/shared/export.errors.ts | 4 +- .../lib/import-export/shared/import.errors.ts | 20 ++++++- .../import-export/shared/import.service.ts | 18 +++++- .../lib/import-export/shared/import.utils.ts | 59 ++++++++++++++++++- packages/geo/src/locale/en.geo.json | 13 +++- packages/geo/src/locale/fr.geo.json | 15 ++++- 18 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 demo/src/app/geo/import-export/import-export-routing.module.ts create mode 100644 demo/src/app/geo/import-export/import-export.component.html create mode 100644 demo/src/app/geo/import-export/import-export.component.scss create mode 100644 demo/src/app/geo/import-export/import-export.component.ts create mode 100644 demo/src/app/geo/import-export/import-export.module.ts diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html index 900fbae6a9..e07f90cfc4 100644 --- a/demo/src/app/app.component.html +++ b/demo/src/app/app.component.html @@ -45,6 +45,7 @@

{{title}}

Catalog Search Print + Import/Export Directions Time filter OGC filter diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 95f7d14188..ab67b41e84 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -39,6 +39,7 @@ import { AppQueryModule } from './geo/query/query.module'; import { AppCatalogModule } from './geo/catalog/catalog.module'; import { AppSearchModule } from './geo/search/search.module'; import { AppPrintModule } from './geo/print/print.module'; +import { AppImportExport } from './geo/import-export/import-export.module'; import { AppDirectionsModule } from './geo/directions/directions.module'; import { AppTimeFilterModule } from './geo/time-filter/time-filter.module'; import { AppOgcFilterModule } from './geo/ogc-filter/ogc-filter.module'; @@ -89,6 +90,7 @@ import { AppComponent } from './app.component'; AppCatalogModule, AppSearchModule, AppPrintModule, + AppImportExport, AppDirectionsModule, AppTimeFilterModule, AppOgcFilterModule, diff --git a/demo/src/app/geo/import-export/import-export-routing.module.ts b/demo/src/app/geo/import-export/import-export-routing.module.ts new file mode 100644 index 0000000000..f1a8040768 --- /dev/null +++ b/demo/src/app/geo/import-export/import-export-routing.module.ts @@ -0,0 +1,15 @@ +import { Routes, RouterModule } from '@angular/router'; +import { ModuleWithProviders } from '@angular/core'; + +import { AppImportExportComponent } from './import-export.component'; + +const routes: Routes = [ + { + path: 'import-export', + component: AppImportExportComponent + } +]; + +export const AppImportExportRoutingModule: ModuleWithProviders = RouterModule.forChild( + routes +); diff --git a/demo/src/app/geo/import-export/import-export.component.html b/demo/src/app/geo/import-export/import-export.component.html new file mode 100644 index 0000000000..849620ce06 --- /dev/null +++ b/demo/src/app/geo/import-export/import-export.component.html @@ -0,0 +1,20 @@ + + Geo + Import / Export + +
  • Dependencies: LanguageService
  • + +
    + See the code of this example +
    +
    + + + + + + + +
    + + diff --git a/demo/src/app/geo/import-export/import-export.component.scss b/demo/src/app/geo/import-export/import-export.component.scss new file mode 100644 index 0000000000..d481ae2940 --- /dev/null +++ b/demo/src/app/geo/import-export/import-export.component.scss @@ -0,0 +1,4 @@ +igo-map-browser { + width: 900px; + height: 1000px; +} diff --git a/demo/src/app/geo/import-export/import-export.component.ts b/demo/src/app/geo/import-export/import-export.component.ts new file mode 100644 index 0000000000..c2a40234c2 --- /dev/null +++ b/demo/src/app/geo/import-export/import-export.component.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; + +import { LanguageService } from '@igo2/core'; +import { IgoMap, LayerService } from '@igo2/geo'; + +@Component({ + selector: 'app-import-export', + templateUrl: './import-export.component.html', + styleUrls: ['./import-export.component.scss'] +}) +export class AppImportExportComponent { + public map = new IgoMap({ + controls: { + attribution: { + collapsed: true + } + } + }); + + public view = { + center: [-73, 47.2], + zoom: 9 + }; + + constructor( + private languageService: LanguageService, + private layerService: LayerService + ) { + this.layerService + .createAsyncLayer({ + title: 'Quebec Base Map', + sourceOptions: { + type: 'wmts', + url: '/carto/wmts/1.0.0/wmts', + layer: 'carte_gouv_qc_ro', + matrixSet: 'EPSG_3857', + version: '1.3.0' + } + }) + .subscribe(l => this.map.addLayer(l)); + + } +} diff --git a/demo/src/app/geo/import-export/import-export.module.ts b/demo/src/app/geo/import-export/import-export.module.ts new file mode 100644 index 0000000000..9541865280 --- /dev/null +++ b/demo/src/app/geo/import-export/import-export.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { MatCardModule, MatButtonModule } from '@angular/material'; + +import { IgoMessageModule } from '@igo2/core'; +import { IgoMapModule, IgoImportExportModule } from '@igo2/geo'; + +import { AppImportExportComponent } from './import-export.component'; +import { AppImportExportRoutingModule } from './import-export-routing.module'; + +@NgModule({ + declarations: [AppImportExportComponent], + imports: [ + AppImportExportRoutingModule, + MatCardModule, + MatButtonModule, + IgoMessageModule, + IgoMapModule, + IgoImportExportModule + ], + exports: [AppImportExportComponent] +}) +export class AppImportExport {} diff --git a/demo/src/environments/environment.ts b/demo/src/environments/environment.ts index c2125809ab..5ebbe609a7 100644 --- a/demo/src/environments/environment.ts +++ b/demo/src/environments/environment.ts @@ -28,6 +28,9 @@ export const environment: Environment = { language: { prefix: './locale/' }, + importExport: { + url: 'https://testgeoegl.msp.gouv.qc.ca/apis/ogre' + }, catalog: { sources: [ { diff --git a/packages/geo/src/lib/import-export/import-export.module.ts b/packages/geo/src/lib/import-export/import-export.module.ts index 38b8d2858f..7122aaa17d 100644 --- a/packages/geo/src/lib/import-export/import-export.module.ts +++ b/packages/geo/src/lib/import-export/import-export.module.ts @@ -11,7 +11,7 @@ import { } from '@angular/material'; import { IgoLanguageModule } from '@igo2/core'; -import { IgoKeyValueModule, IgoDrapDropModule } from '@igo2/common'; +import { IgoKeyValueModule, IgoDrapDropModule, IgoSpinnerModule } from '@igo2/common'; import { ImportExportComponent } from './import-export/import-export.component'; import { DropGeoFileDirective } from './shared/drop-geo-file.directive'; @@ -28,6 +28,7 @@ import { DropGeoFileDirective } from './shared/drop-geo-file.directive'; MatFormFieldModule, MatInputModule, IgoLanguageModule, + IgoSpinnerModule, IgoKeyValueModule, IgoDrapDropModule ], diff --git a/packages/geo/src/lib/import-export/import-export/import-export.component.html b/packages/geo/src/lib/import-export/import-export/import-export.component.html index 0b4e1bc8e0..8657764df4 100644 --- a/packages/geo/src/lib/import-export/import-export/import-export.component.html +++ b/packages/geo/src/lib/import-export/import-export/import-export.component.html @@ -13,9 +13,10 @@
    - +
    + +

    {{'igo.geo.importExportForm.importClarifications' | translate}}

    + @@ -57,10 +64,11 @@ + diff --git a/packages/geo/src/lib/import-export/import-export/import-export.component.scss b/packages/geo/src/lib/import-export/import-export/import-export.component.scss index 2eda22baf7..8fcdaee323 100644 --- a/packages/geo/src/lib/import-export/import-export/import-export.component.scss +++ b/packages/geo/src/lib/import-export/import-export/import-export.component.scss @@ -10,3 +10,8 @@ mat-form-field { text-align: center; padding-top: 10px; } + +igo-spinner { + position: absolute; + padding-left: 10px; +} diff --git a/packages/geo/src/lib/import-export/import-export/import-export.component.ts b/packages/geo/src/lib/import-export/import-export/import-export.component.ts index 55e5a52ad4..57c8868a8a 100644 --- a/packages/geo/src/lib/import-export/import-export/import-export.component.ts +++ b/packages/geo/src/lib/import-export/import-export/import-export.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { Subscription, throwError } from 'rxjs'; import { MessageService, LanguageService } from '@igo2/core'; @@ -14,7 +14,10 @@ import { ExportOptions } from '../shared/export.interface'; import { ExportFormat } from '../shared/export.type'; import { ExportService } from '../shared/export.service'; import { ImportService } from '../shared/import.service'; -import { handleFileImportSuccess, handleFileImportError } from '../shared/import.utils'; +import { + handleFileImportSuccess, + handleFileImportError +} from '../shared/import.utils'; @Component({ selector: 'igo-import-export', @@ -22,11 +25,11 @@ import { handleFileImportSuccess, handleFileImportError } from '../shared/import styleUrls: ['./import-export.component.scss'] }) export class ImportExportComponent implements OnDestroy, OnInit { - public form: FormGroup; public formats = ExportFormat; public layers: VectorLayer[]; public inputProj: string = 'EPSG:4326'; + public loading = false; private layers$$: Subscription; @@ -44,10 +47,9 @@ export class ImportExportComponent implements OnDestroy, OnInit { ngOnInit() { this.layers$$ = this.map.layers$.subscribe(layers => { - this.layers = layers - .filter((layer: Layer) => { - return layer instanceof VectorLayer && layer.exportable === true; - }) as VectorLayer[]; + this.layers = layers.filter((layer: Layer) => { + return layer instanceof VectorLayer && layer.exportable === true; + }) as VectorLayer[]; }); } @@ -56,6 +58,7 @@ export class ImportExportComponent implements OnDestroy, OnInit { } importFiles(files: File[]) { + this.loading = true; for (const file of files) { this.importService .import(file, this.inputProj) @@ -67,12 +70,13 @@ export class ImportExportComponent implements OnDestroy, OnInit { } handleExportFormSubmit(data: ExportOptions) { + this.loading = true; const layer = this.map.getLayerById(data.layer); const olFeatures = layer.dataSource.ol.getFeatures(); this.exportService .export(olFeatures, data.format, layer.title, this.map.projection) .subscribe( - () => {}, + () => { this.loading = false;}, (error: Error) => this.onFileExportError(error) ); } @@ -85,14 +89,28 @@ export class ImportExportComponent implements OnDestroy, OnInit { } private onFileImportSuccess(file: File, features: Feature[]) { - handleFileImportSuccess(file, features, this.map, this.messageService, this.languageService); + this.loading = false; + handleFileImportSuccess( + file, + features, + this.map, + this.messageService, + this.languageService + ); } private onFileImportError(file: File, error: Error) { - handleFileImportError(file, error, this.messageService, this.languageService); + this.loading = false; + handleFileImportError( + file, + error, + this.messageService, + this.languageService + ); } private onFileExportError(error: Error) { + this.loading = false; handleFileExportError(error, this.messageService, this.languageService); } } diff --git a/packages/geo/src/lib/import-export/shared/export.errors.ts b/packages/geo/src/lib/import-export/shared/export.errors.ts index 89131ba764..deb0a6eb03 100644 --- a/packages/geo/src/lib/import-export/shared/export.errors.ts +++ b/packages/geo/src/lib/import-export/shared/export.errors.ts @@ -2,14 +2,14 @@ export class ExportError extends Error {} export class ExportInvalidFileError extends ExportError { constructor() { - super('Invalid file.'); + super('Invalid file'); Object.setPrototypeOf(this, ExportInvalidFileError.prototype); } } export class ExportNothingToExportError extends ExportError { constructor() { - super('Nothing to export.'); + super('Nothing to export'); Object.setPrototypeOf(this, ExportNothingToExportError.prototype); } } diff --git a/packages/geo/src/lib/import-export/shared/import.errors.ts b/packages/geo/src/lib/import-export/shared/import.errors.ts index 9d22a5c02c..4d25b3fa4a 100644 --- a/packages/geo/src/lib/import-export/shared/import.errors.ts +++ b/packages/geo/src/lib/import-export/shared/import.errors.ts @@ -2,21 +2,35 @@ export class ImportError extends Error {} export class ImportInvalidFileError extends ImportError { constructor() { - super('Invalid file.'); + super('Invalid file'); Object.setPrototypeOf(this, ImportInvalidFileError.prototype); } } export class ImportUnreadableFileError extends ImportError { constructor() { - super('Failed to read file.'); + super('Failed to read file'); Object.setPrototypeOf(this, ImportUnreadableFileError.prototype); } } export class ImportNothingToImportError extends ImportError { constructor() { - super('Nothing to import.'); + super('Nothing to import'); + Object.setPrototypeOf(this, ImportNothingToImportError.prototype); + } +} + +export class ImportSizeError extends ImportError { + constructor() { + super('File is too large'); + Object.setPrototypeOf(this, ImportNothingToImportError.prototype); + } +} + +export class ImportSRSError extends ImportError { + constructor() { + super('Invalid SRS definition'); Object.setPrototypeOf(this, ImportNothingToImportError.prototype); } } diff --git a/packages/geo/src/lib/import-export/shared/import.service.ts b/packages/geo/src/lib/import-export/shared/import.service.ts index 979b2541c4..0467d8c4b5 100644 --- a/packages/geo/src/lib/import-export/shared/import.service.ts +++ b/packages/geo/src/lib/import-export/shared/import.service.ts @@ -10,7 +10,7 @@ import * as olformat from 'ol/format'; import OlFeature from 'ol/Feature'; import { Feature } from '../../feature/shared/feature.interfaces'; -import { ImportInvalidFileError, ImportUnreadableFileError } from './import.errors'; +import { ImportInvalidFileError, ImportUnreadableFileError, ImportSizeError, ImportSRSError } from './import.errors'; import { computeLayerTitleFromFile, getFileExtension } from './import.utils'; @Injectable({ @@ -71,6 +71,10 @@ export class ImportService { private importAsync(file: File, projectionIn: string, projectionOut: string): Observable { const doImport = (observer: Observer) => { + if (file.size >= 30000000) { + observer.error(new ImportSizeError()); + return; + } const importer = this.getFileImporter(file); if (importer === undefined) { observer.error(new ImportInvalidFileError()); @@ -136,8 +140,16 @@ export class ImportService { observer.complete(); } }, - (error: Error) => { - observer.error(new ImportUnreadableFileError()); + (error: any) => { + error.error.caught = true; + const errMsg = error.error.msg; + if (errMsg === 'No valid files found') { + observer.error(new ImportInvalidFileError()); + } else if (errMsg.startWith("ERROR 1: Failed to process SRS definition")) { + observer.error(new ImportSRSError()); + } else { + observer.error(new ImportUnreadableFileError()); + } } ); } diff --git a/packages/geo/src/lib/import-export/shared/import.utils.ts b/packages/geo/src/lib/import-export/shared/import.utils.ts index 423189c3f8..d612fe7429 100644 --- a/packages/geo/src/lib/import-export/shared/import.utils.ts +++ b/packages/geo/src/lib/import-export/shared/import.utils.ts @@ -56,7 +56,7 @@ export function handleFileImportSuccess( languageService: LanguageService ) { if (features.length === 0) { - this.handleNothingToImportError(file, messageService, languageService); + handleNothingToImportError(file, messageService, languageService); return; } @@ -76,6 +76,21 @@ export function handleFileImportError( error: Error, messageService: MessageService, languageService: LanguageService +) { + const errMapping = { + "Invalid file": handleInvalidFileImportError, + "File is too large": handleSizeFileImportError, + "Failed to read file": handleUnreadbleFileImportError, + "Invalid SRS definition": handleSRSImportError + } + errMapping[error.message](file, error, messageService, languageService); +} + +export function handleInvalidFileImportError( + file: File, + error: Error, + messageService: MessageService, + languageService: LanguageService ) { const translate = languageService.translate; const title = translate.instant('igo.geo.dropGeoFile.invalid.title'); @@ -86,6 +101,34 @@ export function handleFileImportError( messageService.error(message, title); } +export function handleUnreadbleFileImportError( + file: File, + error: Error, + messageService: MessageService, + languageService: LanguageService +) { + const translate = languageService.translate; + const title = translate.instant('igo.geo.dropGeoFile.unreadable.title'); + const message = translate.instant('igo.geo.dropGeoFile.unreadable.text', { + value: file.name + }); + messageService.error(message, title); +} + +export function handleSizeFileImportError( + file: File, + error: Error, + messageService: MessageService, + languageService: LanguageService +) { + const translate = languageService.translate; + const title = translate.instant('igo.geo.dropGeoFile.tooLarge.title'); + const message = translate.instant('igo.geo.dropGeoFile.tooLarge.text', { + value: file.name + }); + messageService.error(message, title); +} + export function handleNothingToImportError( file: File, messageService: MessageService, @@ -100,6 +143,20 @@ export function handleNothingToImportError( messageService.error(message, title); } +export function handleSRSImportError( + file: File, + messageService: MessageService, + languageService: LanguageService +) { + const translate = languageService.translate; + const title = translate.instant('igo.geo.dropGeoFile.invalidSRS.title'); + const message = translate.instant('igo.geo.dropGeoFile.invalidSRS.text', { + value: file.name, + mimeType: file.type + }); + messageService.error(message, title); +} + export function getFileExtension(file: File): string { return file.name.split('.').pop().toLowerCase(); } diff --git a/packages/geo/src/locale/en.geo.json b/packages/geo/src/locale/en.geo.json index 5d7a46e3ff..386e30e8e9 100644 --- a/packages/geo/src/locale/en.geo.json +++ b/packages/geo/src/locale/en.geo.json @@ -30,6 +30,14 @@ "unreadable": { "text": "Unreadable file", "title": "The file '{{value}}' is unreadable" + }, + "tooLarge": { + "text": "The file '{{value}}' is too large (Max: 30 mb)", + "title": "File too large" + }, + "invalidSRS": { + "text": "The projection is valid. It must start with EPSG:", + "title": "Invalid projection" } }, "export": { @@ -49,7 +57,10 @@ "exportTabTitle": "Export", "importButton": "Import", "importProjPlaceholder": "Coordinate system", - "importTabTitle": "Import" + "importTabTitle": "Import", + "importClarifications": "Clarifications", + "importSizeMax": "The file size limit is 30 mb.", + "importShpZip": "Shapefiles must be zipped." }, "operators": { "And": "And", diff --git a/packages/geo/src/locale/fr.geo.json b/packages/geo/src/locale/fr.geo.json index 959e009b28..2900f8cf56 100644 --- a/packages/geo/src/locale/fr.geo.json +++ b/packages/geo/src/locale/fr.geo.json @@ -20,7 +20,7 @@ "title": "Fichier invalide" }, "empty": { - "text": "Le fichier '{{value}}' est vide, impossible d'importer.", + "text": "Le fichier '{{value}}' est vide, impossible d'importer", "title": "Fichier vide" }, "success": { @@ -30,6 +30,14 @@ "unreadable": { "text": "Le fichier '{{value}}' est impossible à lire", "title": "Fichier illisible" + }, + "tooLarge": { + "text": "Le fichier '{{value}}' est trop volumineux (Maximun: 30 mb)", + "title": "Fichier trop volumineux" + }, + "invalidSRS": { + "text": "La projection est valide. Elle doit commencer par EPSG:", + "title": "Projection invalid" } }, "export": { @@ -49,7 +57,10 @@ "exportTabTitle": "Exporter", "importButton": "Importer", "importProjPlaceholder": "Système de coordonnées", - "importTabTitle": "Importer" + "importTabTitle": "Importer", + "importClarifications": "Précisions", + "importSizeMax": "La taille limite du fichier est de 30 mb.", + "importShpZip": "Les shapefiles doivent être compressés (zippés)" }, "operators": { "And": "Et",