-
Notifications
You must be signed in to change notification settings - Fork 32
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
Changes from 4 commits
2ad8288
edbe1ea
8a497f1
ffc87e2
0c8d1af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
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(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
There was a problem hiding this comment.
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.