Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Map-Viewer] Added layer from a GeoJSON file #777

Merged
merged 5 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="flex flex-col gap-2 my-2">
<div class="flex items-center gap-4">
<div class="flex-grow rounded-md border-2 border-gray-200">
<gn-ui-drag-and-drop-file-input
(fileChange)="handleFileChange($event)"
[accept]="acceptedMimeType.join(',')"
[placeholder]="'map.addFromFile.placeholder' | translate"
class="placeholder-grey"
></gn-ui-drag-and-drop-file-input>
</div>
</div>
<p class="text-sm text-gray-600" translate>map.help.addFromFile</p>
</div>

<div *ngIf="errorMessage" class="text-red-500 mt-2">
{{ errorMessage }}
</div>

<div *ngIf="successMessage" class="text-green-500 mt-2">
{{ successMessage }}
</div>
Original file line number Diff line number Diff line change
@@ -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<AddLayerFromFileComponent>
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('if file is not selected', () => {
beforeEach(() => {
component.handleFileChange(null)
})
it('should set error message', () => {
expect(component.errorMessage).toEqual('Invalid file format')
})
})
describe('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('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),
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
loading = false
readonly acceptedMimeType = ['.geojson']
readonly maxFileSize = 5000000

constructor(
private mapFacade: MapFacade,
private changeDetectorRef: ChangeDetectorRef
) {}

async handleFileChange(file: File) {
if (!file) {
this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error')
return
}
if (file.size > this.maxFileSize) {
this.displayMessage('File size exceeds the limit of 5MB', 'error')
return
}
await this.addLayer(file)
}

private async addLayer(file: File) {
this.errorMessage = null
this.loading = true
try {
if (!this.isFileFormatValid(file)) {
this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error')
return
}

const fileExtension = this.getFileExtension(file)
switch (fileExtension) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One improvement here would be to use DuckDB spatial WASM, to be able to load any kind of spatial dataset.

case 'geojson':
await this.addGeoJsonLayer(file)
break
default:
this.displayMessage(INVALID_FILE_FORMAT_ERROR_MESSAGE, 'error')
break
}
} catch (error) {
const err = error as Error
this.displayMessage('Error loading file: ' + err.message, 'error')
} finally {
this.loading = false
}
}

private addGeoJsonLayer(file: File) {
return new Promise<void>((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.displayMessage('File successfully added to map', 'success')
resolve()
}
reader.onerror = reject
reader.readAsText(file)
} catch (error) {
reject(error)
}
})
}

private isFileFormatValid(file: File): boolean {
const fileExtension = this.getFileExtension(file)
return this.acceptedMimeType.includes(`.${fileExtension}`)
}

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(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jahow do we have a notification système ? Could be another task for @ronitjadhav what do you guys think ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no global notification system and we might not need one, since for now all errors are "situational". Let's keep it like that for now.

if (type === 'success') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition might not be necessary, you can just set the values to null (as discussed).

this.successMessage = null
} else if (type === 'error') {
this.errorMessage = null
}
this.changeDetectorRef.detectChanges()
}, 5000)
}
}
2 changes: 2 additions & 0 deletions libs/feature/map/src/lib/feature-map.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -31,6 +32,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm
MapContainerComponent,
AddLayerRecordPreviewComponent,
AddLayerFromWmsComponent,
AddLayerFromFileComponent,
],
exports: [
MapContextComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
<div class="p-3 h-full">Add from WFS</div>
</mat-tab>
<mat-tab [label]="'map.add.layer.file' | translate" bodyClass="h-full">
<div class="p-3 h-full">Add from file</div>
<div class="p-3">
<gn-ui-add-layer-from-file></gn-ui-add-layer-from-file>
</div>
</mat-tab>
</mat-tab-group>
</gn-ui-expandable-panel-button>
Expand Down
2 changes: 2 additions & 0 deletions translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
2 changes: 2 additions & 0 deletions translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
2 changes: 2 additions & 0 deletions translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
2 changes: 2 additions & 0 deletions translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
2 changes: 2 additions & 0 deletions translations/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down