From 2ad8288aa644bfdcde415666658f86144375fb18 Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Fri, 19 Jan 2024 11:54:48 +0100 Subject: [PATCH 1/4] Added a component to load layers from a file (GeoJson) --- .../add-layer-from-file.component.css | 0 .../add-layer-from-file.component.html | 21 +++ .../add-layer-from-file.component.spec.ts | 128 ++++++++++++++++++ .../add-layer-from-file.component.ts | 103 ++++++++++++++ .../feature/map/src/lib/feature-map.module.ts | 2 + .../layers-panel/layers-panel.component.html | 4 +- translations/de.json | 2 + translations/en.json | 2 + translations/es.json | 2 + translations/fr.json | 2 + translations/it.json | 2 + translations/nl.json | 2 + translations/pt.json | 2 + translations/sk.json | 2 + 14 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.css create mode 100644 libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html create mode 100644 libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts create mode 100644 libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.css b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html new file mode 100644 index 0000000000..0cc93447b6 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html @@ -0,0 +1,21 @@ +
+
+
+ +
+
+

map.help.addFromFile

+
+ +
+ {{ errorMessage }} +
+ +
+ {{ successMessage }} +
diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts new file mode 100644 index 0000000000..06f7ea5a09 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts @@ -0,0 +1,128 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { AddLayerFromFileComponent } from './add-layer-from-file.component' +import { MapFacade } from '../+state/map.facade' +import { TranslateModule } from '@ngx-translate/core' + +class MapFacadeMock { + addLayer = jest.fn() +} + +describe('AddLayerFromFileComponent', () => { + let component: AddLayerFromFileComponent + let fixture: ComponentFixture + let mapFacade: MapFacade + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AddLayerFromFileComponent], + providers: [ + { + provide: MapFacade, + useClass: MapFacadeMock, + }, + ], + }).compileComponents() + + mapFacade = TestBed.inject(MapFacade) + fixture = TestBed.createComponent(AddLayerFromFileComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + expect(component.errorMessage).toBeFalsy() + expect(component.loading).toBe(false) + expect(component.successMessage).toBeFalsy() + }) + + describe('handleFileChange', () => { + describe('should set error message if file is not selected', () => { + beforeEach(() => { + component.handleFileChange(null) + }) + it('should set error message', () => { + expect(component.errorMessage).toEqual('File is invalid') + }) + }) + describe('should set error message if file size exceeds the limit', () => { + beforeEach(() => { + const file = new File([''], 'filename', { type: 'text/plain' }) + jest.spyOn(file, 'size', 'get').mockReturnValue(5000001) + component.handleFileChange(file) + }) + it('should set error message', () => { + expect(component.errorMessage).toEqual( + 'File size exceeds the limit of 5MB' + ) + }) + }) + describe('should set error message if file format is invalid', () => { + beforeEach(() => { + const file = new File([''], 'filename', { type: 'text/plain' }) + component.handleFileChange(file) + }) + it('should set error message', () => { + expect(component.errorMessage).toEqual('Invalid file format') + }) + }) + describe('Invalid and then valid file', () => { + beforeEach(async () => { + const file = new File([''], 'filename', { type: 'text/plain' }) + await component.handleFileChange(file).catch(() => { + // ignore + }) + const file2 = new File([''], 'filename.geojson', { + type: 'application/json', + }) + await component.handleFileChange(file2) + }) + it('should show no error', () => { + expect(component.errorMessage).toBeFalsy() + }) + }) + }) + describe('addGeoJsonLayer', () => { + let data // define data here + + beforeEach(async () => { + // make this async + data = { + type: 'Feature', + properties: { + id: '0', + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ], + ], + }, + } + + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }) + const file = new File([blob], 'filename.geojson', { + type: 'application/json', + }) + + await component.handleFileChange(file) // await this + }) + + it('should add the layer', () => { + expect(mapFacade.addLayer).toHaveBeenCalledWith({ + type: 'geojson', + title: 'filename', + data: JSON.stringify(data, null, 2), + }) + }) + }) +}) diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts new file mode 100644 index 0000000000..4770a35820 --- /dev/null +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts @@ -0,0 +1,103 @@ +import { ChangeDetectorRef, Component } from '@angular/core' +import { MapContextLayerModel } from '../map-context/map-context.model' +import { MapFacade } from '../+state/map.facade' + +@Component({ + selector: 'gn-ui-add-layer-from-file', + templateUrl: './add-layer-from-file.component.html', + styleUrls: ['./add-layer-from-file.component.css'], +}) +export class AddLayerFromFileComponent { + errorMessage: string | null = null + successMessage: string | null = null + loading = false + readonly acceptedMimeType = ['.geojson'] + readonly maxFileSize = 5000000 + + constructor( + private mapFacade: MapFacade, + private changeDetectorRef: ChangeDetectorRef + ) {} + + async handleFileChange(file: File) { + if (!file) { + this.errorMessage = 'File is invalid' + this.displayError() + return + } + if (file.size > this.maxFileSize) { + this.errorMessage = 'File size exceeds the limit of 5MB' + this.displayError() + return + } + await this.addLayer(file) + } + + private async addLayer(file: File) { + this.errorMessage = null + this.loading = true + try { + if (!this.isFileFormatValid(file)) { + this.errorMessage = 'Invalid file format' + this.displayError() + return + } + + const fileExtension = file.name.split('.').pop() + switch (fileExtension) { + case 'geojson': + await this.addGeoJsonLayer(file) + break + default: + this.errorMessage = 'Invalid file format' + this.displayError() + break + } + } catch (error) { + const err = error as Error + this.errorMessage = 'Error loading file: ' + err.message + this.displayError() + } finally { + this.loading = false + } + } + + private addGeoJsonLayer(file: File) { + return new Promise((resolve, reject) => { + try { + const reader = new FileReader() + reader.onload = () => { + const result = reader.result as string + const title = file.name.split('.').slice(0, -1).join('.') + const layerToAdd: MapContextLayerModel = { + type: 'geojson', + data: result, + } + this.mapFacade.addLayer({ ...layerToAdd, title: title }) + this.successMessage = 'File successfully added to map' + setTimeout(() => { + this.successMessage = null + this.changeDetectorRef.detectChanges() + }, 5000) + resolve() + } + reader.onerror = reject + reader.readAsText(file) + } catch (error) { + reject(error) + } + }) + } + + private isFileFormatValid(file: File): boolean { + const fileExtension = file.name.split('.').pop() + return this.acceptedMimeType.includes(`.${fileExtension}`) + } + + private displayError() { + setTimeout(() => { + this.errorMessage = null + this.changeDetectorRef.detectChanges() + }, 5000) + } +} diff --git a/libs/feature/map/src/lib/feature-map.module.ts b/libs/feature/map/src/lib/feature-map.module.ts index facd91d161..5737dee3e9 100644 --- a/libs/feature/map/src/lib/feature-map.module.ts +++ b/libs/feature/map/src/lib/feature-map.module.ts @@ -21,6 +21,7 @@ import { AddLayerRecordPreviewComponent } from './add-layer-from-catalog/add-lay import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wms.component' +import { AddLayerFromFileComponent } from './add-layer-from-file/add-layer-from-file.component' @NgModule({ declarations: [ @@ -31,6 +32,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm MapContainerComponent, AddLayerRecordPreviewComponent, AddLayerFromWmsComponent, + AddLayerFromFileComponent, ], exports: [ MapContextComponent, diff --git a/libs/feature/map/src/lib/layers-panel/layers-panel.component.html b/libs/feature/map/src/lib/layers-panel/layers-panel.component.html index 61454e4e23..0cf4df0112 100644 --- a/libs/feature/map/src/lib/layers-panel/layers-panel.component.html +++ b/libs/feature/map/src/lib/layers-panel/layers-panel.component.html @@ -39,7 +39,9 @@
Add from WFS
-
Add from file
+
+ +
diff --git a/translations/de.json b/translations/de.json index 00048b25b0..545022f641 100644 --- a/translations/de.json +++ b/translations/de.json @@ -175,6 +175,8 @@ "map.add.layer.file": "Aus einer Datei", "map.add.layer.wfs": "Aus WFS", "map.add.layer.wms": "Aus WMS", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "Ebenen", diff --git a/translations/en.json b/translations/en.json index 3fb97bc19c..d88988b81b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -175,6 +175,8 @@ "map.add.layer.file": "From a file", "map.add.layer.wfs": "From WFS", "map.add.layer.wms": "From WMS", + "map.addFromFile.placeholder": "Click or drop a file here", + "map.help.addFromFile": "Click or drag and drop a file to add to the map (currently supports GeoJSON format only).", "map.layer.add": "Add", "map.layers.available": "Available Layers", "map.layers.list": "Layers", diff --git a/translations/es.json b/translations/es.json index dea04b3d11..b09de24e42 100644 --- a/translations/es.json +++ b/translations/es.json @@ -175,6 +175,8 @@ "map.add.layer.file": "", "map.add.layer.wfs": "", "map.add.layer.wms": "", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "", diff --git a/translations/fr.json b/translations/fr.json index edea41d945..dc12283fb0 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -175,6 +175,8 @@ "map.add.layer.file": "", "map.add.layer.wfs": "", "map.add.layer.wms": "", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "", diff --git a/translations/it.json b/translations/it.json index d4bf47de5e..ac0dd5912e 100644 --- a/translations/it.json +++ b/translations/it.json @@ -175,6 +175,8 @@ "map.add.layer.file": "Da un file", "map.add.layer.wfs": "Da un WFS", "map.add.layer.wms": "Da un WMS", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "Layers", diff --git a/translations/nl.json b/translations/nl.json index 44445b67bc..ca63b4edf5 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -175,6 +175,8 @@ "map.add.layer.file": "", "map.add.layer.wfs": "", "map.add.layer.wms": "", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "", diff --git a/translations/pt.json b/translations/pt.json index 3f1d703751..d6107e81a6 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -175,6 +175,8 @@ "map.add.layer.file": "", "map.add.layer.wfs": "", "map.add.layer.wms": "", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "", diff --git a/translations/sk.json b/translations/sk.json index bc379dd3cd..9271be5ff3 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -175,6 +175,8 @@ "map.add.layer.file": "Zo súboru", "map.add.layer.wfs": "Z WFS", "map.add.layer.wms": "Z WMS", + "map.addFromFile.placeholder": "", + "map.help.addFromFile": "", "map.layer.add": "", "map.layers.available": "", "map.layers.list": "Vrstvy", From edbe1ea184b014f0789eceb298a442195936e0fd Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Tue, 23 Jan 2024 14:05:32 +0100 Subject: [PATCH 2/4] resloved the comments on PR --- .../add-layer-from-file.component.spec.ts | 6 +-- .../add-layer-from-file.component.ts | 51 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts index 06f7ea5a09..a785d5753e 100644 --- a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts @@ -38,7 +38,7 @@ describe('AddLayerFromFileComponent', () => { }) describe('handleFileChange', () => { - describe('should set error message if file is not selected', () => { + describe('if file is not selected', () => { beforeEach(() => { component.handleFileChange(null) }) @@ -46,7 +46,7 @@ describe('AddLayerFromFileComponent', () => { expect(component.errorMessage).toEqual('File is invalid') }) }) - describe('should set error message if file size exceeds the limit', () => { + describe('if file size exceeds the limit', () => { beforeEach(() => { const file = new File([''], 'filename', { type: 'text/plain' }) jest.spyOn(file, 'size', 'get').mockReturnValue(5000001) @@ -58,7 +58,7 @@ describe('AddLayerFromFileComponent', () => { ) }) }) - describe('should set error message if file format is invalid', () => { + describe('if file format is invalid', () => { beforeEach(() => { const file = new File([''], 'filename', { type: 'text/plain' }) component.handleFileChange(file) diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts index 4770a35820..838210ca7e 100644 --- a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts @@ -2,11 +2,15 @@ import { ChangeDetectorRef, Component } from '@angular/core' import { MapContextLayerModel } from '../map-context/map-context.model' import { MapFacade } from '../+state/map.facade' +const INVALID_FILE_FORMAT_ERROR_MESSAGE = 'Invalid file format' + @Component({ selector: 'gn-ui-add-layer-from-file', templateUrl: './add-layer-from-file.component.html', styleUrls: ['./add-layer-from-file.component.css'], }) + + export class AddLayerFromFileComponent { errorMessage: string | null = null successMessage: string | null = null @@ -21,13 +25,11 @@ export class AddLayerFromFileComponent { async handleFileChange(file: File) { if (!file) { - this.errorMessage = 'File is invalid' - this.displayError() + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); return } if (file.size > this.maxFileSize) { - this.errorMessage = 'File size exceeds the limit of 5MB' - this.displayError() + this.displayMessage('File size exceeds the limit of 5MB', 'error'); return } await this.addLayer(file) @@ -38,25 +40,22 @@ export class AddLayerFromFileComponent { this.loading = true try { if (!this.isFileFormatValid(file)) { - this.errorMessage = 'Invalid file format' - this.displayError() + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); return } - const fileExtension = file.name.split('.').pop() + const fileExtension = this.getFileExtension(file) switch (fileExtension) { case 'geojson': await this.addGeoJsonLayer(file) break default: - this.errorMessage = 'Invalid file format' - this.displayError() + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); break } } catch (error) { const err = error as Error - this.errorMessage = 'Error loading file: ' + err.message - this.displayError() + this.displayMessage('Error loading file: ' + err.message, 'error'); } finally { this.loading = false } @@ -74,11 +73,7 @@ export class AddLayerFromFileComponent { data: result, } this.mapFacade.addLayer({ ...layerToAdd, title: title }) - this.successMessage = 'File successfully added to map' - setTimeout(() => { - this.successMessage = null - this.changeDetectorRef.detectChanges() - }, 5000) + this.displayMessage('File successfully added to map', 'success'); resolve() } reader.onerror = reject @@ -90,14 +85,28 @@ export class AddLayerFromFileComponent { } private isFileFormatValid(file: File): boolean { - const fileExtension = file.name.split('.').pop() + const fileExtension = this.getFileExtension(file) return this.acceptedMimeType.includes(`.${fileExtension}`) } - private displayError() { + private getFileExtension(file: File): string | undefined { + return file.name.split('.').pop(); + } + + private displayMessage(message: string, type: 'success' | 'error') { + if (type === 'success') { + this.successMessage = message; + } else if (type === 'error') { + this.errorMessage = message; + } + setTimeout(() => { - this.errorMessage = null - this.changeDetectorRef.detectChanges() - }, 5000) + if (type === 'success') { + this.successMessage = null; + } else if (type === 'error') { + this.errorMessage = null; + } + this.changeDetectorRef.detectChanges(); + }, 5000); } } From 8a497f196db96e36fe65c8cb1aecb73726383e86 Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Tue, 23 Jan 2024 14:11:44 +0100 Subject: [PATCH 3/4] resloved formatting issue --- .../add-layer-from-file.component.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts index 838210ca7e..f7ea957150 100644 --- a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.ts @@ -9,8 +9,6 @@ const INVALID_FILE_FORMAT_ERROR_MESSAGE = 'Invalid file format' templateUrl: './add-layer-from-file.component.html', styleUrls: ['./add-layer-from-file.component.css'], }) - - export class AddLayerFromFileComponent { errorMessage: string | null = null successMessage: string | null = null @@ -25,11 +23,11 @@ export class AddLayerFromFileComponent { async handleFileChange(file: File) { if (!file) { - this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error') return } if (file.size > this.maxFileSize) { - this.displayMessage('File size exceeds the limit of 5MB', 'error'); + this.displayMessage('File size exceeds the limit of 5MB', 'error') return } await this.addLayer(file) @@ -40,7 +38,7 @@ export class AddLayerFromFileComponent { this.loading = true try { if (!this.isFileFormatValid(file)) { - this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error') return } @@ -50,12 +48,12 @@ export class AddLayerFromFileComponent { await this.addGeoJsonLayer(file) break default: - this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error'); + this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error') break } } catch (error) { const err = error as Error - this.displayMessage('Error loading file: ' + err.message, 'error'); + this.displayMessage('Error loading file: ' + err.message, 'error') } finally { this.loading = false } @@ -73,7 +71,7 @@ export class AddLayerFromFileComponent { data: result, } this.mapFacade.addLayer({ ...layerToAdd, title: title }) - this.displayMessage('File successfully added to map', 'success'); + this.displayMessage('File successfully added to map', 'success') resolve() } reader.onerror = reject @@ -90,23 +88,23 @@ export class AddLayerFromFileComponent { } private getFileExtension(file: File): string | undefined { - return file.name.split('.').pop(); + return file.name.split('.').pop() } private displayMessage(message: string, type: 'success' | 'error') { if (type === 'success') { - this.successMessage = message; + this.successMessage = message } else if (type === 'error') { - this.errorMessage = message; + this.errorMessage = message } setTimeout(() => { if (type === 'success') { - this.successMessage = null; + this.successMessage = null } else if (type === 'error') { - this.errorMessage = null; + this.errorMessage = null } - this.changeDetectorRef.detectChanges(); - }, 5000); + this.changeDetectorRef.detectChanges() + }, 5000) } } From ffc87e22803c64e087c1d31100f4fd03004ec768 Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Tue, 23 Jan 2024 14:23:19 +0100 Subject: [PATCH 4/4] resloved unit test issue and resolved PR comment --- .../add-layer-from-file/add-layer-from-file.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts index a785d5753e..a0eed6a5f2 100644 --- a/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts +++ b/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.spec.ts @@ -43,7 +43,7 @@ describe('AddLayerFromFileComponent', () => { component.handleFileChange(null) }) it('should set error message', () => { - expect(component.errorMessage).toEqual('File is invalid') + expect(component.errorMessage).toEqual('Invalid file format') }) }) describe('if file size exceeds the limit', () => {