Skip to content

Commit

Permalink
feat(importExport): new export method by URL (based on download prope…
Browse files Browse the repository at this point in the history
…rty) (#661)

* fix(importExport)Skip while exportOptions undefined

* refactor(importExport) typo

* feat(import-export) formats is now a observable

* feat(import  export) Warning on no exportable layers available

* feat(importExport) New export method bu URL (based on download property)

* feat(import-export) manage downloadable format

* refactor(locale) typo

* feat(import-export)layer list is now observable

* refactor(import-export) delay on url openning

* refactor(import-export) clear the export format form if not contained in the format list

* feat(import-export) try to preload features for not loaded layer/empty layers

* feat(*) export button for exportable layers

* feat(map) moving from download button to export button
  • Loading branch information
pelord authored Jun 9, 2020
1 parent df28ce7 commit f1af5ed
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<button
*ngIf="layerIsExportable()"
mat-icon-button
tooltip-position="below"
matTooltipShowDelay="500"
[matTooltip]="'igo.geo.download.action' | translate"
[color]="color">
<!-- (click)="openDownload(layer)"> -->
<mat-icon svgIcon="file-export"></mat-icon>
</button>

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

import { Layer } from '../../layer/shared/layers/layer';

import { LayerOptions, VectorLayer } from '../../layer';

@Component({
selector: 'igo-export-button',
templateUrl: './export-button.component.html',
styleUrls: ['./export-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExportButtonComponent {
@Input()
get layer(): Layer {
return this._layer;
}
set layer(value: Layer) {
this._layer = value;
}
private _layer: Layer;

@Input()
get color() {
return this._color;
}
set color(value: string) {
this._color = value;
}
private _color = 'primary';

constructor() {}

get options(): LayerOptions {
if (!this.layer) {
return;
}
return this.layer.dataSource.options;
}

layerIsExportable(): boolean {
if ((this.layer instanceof VectorLayer && this.layer.exportable === true) ||
(this.layer.dataSource.options.download && this.layer.dataSource.options.download.url)) {
return true;
}
return false;
}
}
1 change: 1 addition & 0 deletions packages/geo/src/lib/import-export/export-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './export-button.component';
11 changes: 8 additions & 3 deletions packages/geo/src/lib/import-export/import-export.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@ import {
MatOptionModule,
MatFormFieldModule,
MatInputModule,
MatSlideToggleModule
MatSlideToggleModule,
MatIconModule,
MatTooltipModule
} from '@angular/material';

import { IgoLanguageModule } from '@igo2/core';
import { IgoKeyValueModule, IgoDrapDropModule, IgoSpinnerModule } from '@igo2/common';

import { ExportButtonComponent } from './export-button/export-button.component';
import { ImportExportComponent } from './import-export/import-export.component';
import { DropGeoFileDirective } from './shared/drop-geo-file.directive';
import { IgoStyleListModule } from './style-list/style-list.module';

@NgModule({
imports: [
MatIconModule,
MatTooltipModule,
FormsModule,
ReactiveFormsModule,
CommonModule,
Expand All @@ -36,8 +41,8 @@ import { IgoStyleListModule } from './style-list/style-list.module';
IgoDrapDropModule,
IgoStyleListModule.forRoot()
],
exports: [ImportExportComponent, DropGeoFileDirective, IgoStyleListModule],
declarations: [ImportExportComponent, DropGeoFileDirective]
exports: [ImportExportComponent, DropGeoFileDirective, IgoStyleListModule, ExportButtonComponent],
declarations: [ImportExportComponent, DropGeoFileDirective, ExportButtonComponent]
})
export class IgoImportExportModule {
static forRoot(): ModuleWithProviders {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ <h4>{{'igo.geo.importExportForm.importClarifications' | translate}}</h4>
</mat-tab>

<mat-tab [label]="'igo.geo.importExportForm.exportTabTitle' | translate">
<form class="igo-form" [formGroup]="form">
<section class="mat-typography" *ngIf="(exportableLayers$ | async).length === 0">
<h4>{{'igo.geo.importExportForm.exportNoLayersExportable' | translate}}</h4>
</section>

<form class="igo-form" [formGroup]="form" *ngIf="(exportableLayers$ | async).length > 0">
<div class="igo-input-container">
<mat-form-field>
<mat-select
formControlName="layer"
placeholder="{{'igo.geo.importExportForm.exportLayerPlaceholder' | translate}}">
<mat-option *ngFor="let layer of layers" [value]="layer.id">
<mat-option *ngFor="let layer of (exportableLayers$ | async)" [value]="layer.id">
{{layer.title}}
</mat-option>
</mat-select>
Expand All @@ -57,20 +60,20 @@ <h4>{{'igo.geo.importExportForm.importClarifications' | translate}}</h4>
<mat-select
formControlName="format"
placeholder="{{'igo.geo.importExportForm.exportFormatPlaceholder' | translate}}">
<mat-option *ngFor="let format of formats | keyvalue " [value]="format.key">
<mat-option *ngFor="let format of (formats$ | async) | keyvalue " [value]="format.key">
{{format.value}}
</mat-option>
</mat-select>
</mat-form-field>
</div>

<div class="igo-input-container" *ngIf="forceNaming">
<div class="igo-input-container" *ngIf="forceNaming && form.value.format !== 'URL'">
<mat-form-field>
<input matInput formControlName="name" placeholder="{{'igo.geo.importExportForm.exportFileNamePlaceholder' | translate}}">
</mat-form-field>
</div>

<div class="export-options mat-typography">
<div class="export-options mat-typography" *ngIf="form.value.format !== 'URL'">
<mat-slide-toggle
formControlName="featureInMapExtent"
[labelPosition]="'before'">
Expand All @@ -84,7 +87,7 @@ <h4>{{'igo.geo.importExportForm.importClarifications' | translate}}</h4>
type="button"
[disabled]="!form.valid || (loading$ | async)"
(click)="handleExportFormSubmit(form.value)">
{{'igo.geo.importExportForm.exportButton' | translate}}
{{form.value.format !== 'URL' ? ('igo.geo.importExportForm.exportButton' | translate): ('igo.geo.importExportForm.exportButtonLink' | translate)}}
</button>
<igo-spinner [shown]="loading$ | async"></igo-spinner>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { IgoMap } from '../../map/shared/map';
import { ClusterDataSource } from '../../datasource/shared/datasources/cluster-datasource';
import { Layer } from '../../layer/shared/layers/layer';
import { VectorLayer } from '../../layer/shared/layers/vector-layer';
import { AnyLayer } from '../../layer/shared/layers/any-layer';
import { DataSourceOptions } from '../../datasource/shared/datasources/datasource.interface';

import { handleFileExportError } from '../shared/export.utils';
import { ExportOptions } from '../shared/export.interface';
Expand All @@ -32,20 +34,26 @@ import { skipWhile } from 'rxjs/operators';
})
export class ImportExportComponent implements OnDestroy, OnInit {
public form: FormGroup;
public formats;
public layers: VectorLayer[];
public formats$ = new BehaviorSubject(undefined);
public exportableLayers$: BehaviorSubject<AnyLayer[]> = new BehaviorSubject([]);
public inputProj: string = 'EPSG:4326';
public loading$ = new BehaviorSubject(false);
public forceNaming = false;

private layers$$: Subscription;
private form$$: Subscription;
private exportableLayers$$: Subscription;
private formats$$: Subscription;
private formLayer$$: Subscription;
private exportOptions$$: Subscription;

private espgCodeRegex = new RegExp('^\\d{4,6}');
private clientSideFileSizeMax: number;
public fileSizeMb: number;

private previousLayerSpecs$: BehaviorSubject<
{ id: string, visible: boolean, opacity: number, queryable: boolean }
> = new BehaviorSubject(undefined);

@Input() map: IgoMap;

@Input() selectedIndex: number = 0;
Expand All @@ -72,9 +80,10 @@ 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.exportableLayers$.next(layers.filter((layer: Layer) => {
return (layer instanceof VectorLayer && layer.exportable === true) ||
(layer.dataSource.options.download && layer.dataSource.options.download.url);
}) as AnyLayer[]);
});
const configFileSizeMb = this.config.getConfig(
'importExport.clientSideFileSizeMaxMb'
Expand All @@ -86,20 +95,71 @@ export class ImportExportComponent implements OnDestroy, OnInit {
this.exportOptions$$ = this.exportOptions$
.pipe(skipWhile(exportOptions => !exportOptions))
.subscribe((exportOptions) => {
this.form.patchValue(exportOptions, { emitEvent: false });
this.form.patchValue(exportOptions, { emitEvent: true });
if (exportOptions.layer) {
this.computeFormats(this.map.getLayerById(exportOptions.layer));
}
});

this.formLayer$$ = this.form.get('layer').valueChanges.subscribe((layerId) => {
this.handlePreviousLayerSpecs();
const layer = this.map.getLayerById(layerId);
this.computeFormats(layer);
if (Object.keys(this.formats$.value).indexOf(this.form.value.format) === -1) {
this.form.patchValue({ format: undefined });
}

if (layer instanceof VectorLayer && layer.dataSource.ol.getFeatures().length === 0) {
this.loading$.next(true);
this.previousLayerSpecs$.next(
{ id: layerId, visible: layer.visible, opacity: layer.opacity, queryable: (layer as any).queryable }
);
layer.opacity = 0;
layer.visible = true;
setTimeout(() => {
this.loading$.next(false);
}, 500);
}
});

this.formats$$ = this.formats$
.pipe(skipWhile(formats => !formats))
.subscribe(formats => {
if (Object.keys(formats).length === 1) {
this.form.patchValue({ format: formats[Object.keys(formats)[0]] });
}
});

this.form$$ = this.form.valueChanges.subscribe(() => {
this.exportOptionsChange.emit(this.form.value);
});
this.exportableLayers$$ = this.exportableLayers$
.pipe(skipWhile(layers => !layers))
.subscribe(layers => {
if (layers.length === 1) {
this.form.patchValue({ layer: layers[0].id });
}
});
}

ngOnDestroy() {
this.layers$$.unsubscribe();
this.form$$.unsubscribe();
this.exportableLayers$$.unsubscribe();
this.formats$$.unsubscribe();
this.formLayer$$.unsubscribe();
if (this.exportOptions$$) {
this.exportOptions$$.unsubscribe();
}
this.exportOptionsChange.emit(this.form.value);
this.handlePreviousLayerSpecs();

}

private handlePreviousLayerSpecs() {
if (this.previousLayerSpecs$.value) {
const previousLayer = this.map.getLayerById(this.previousLayerSpecs$.value.id);
previousLayer.visible = this.previousLayerSpecs$.value.visible;
previousLayer.opacity = this.previousLayerSpecs$.value.opacity;
(previousLayer as any).queryable = this.previousLayerSpecs$.value.queryable;
this.previousLayerSpecs$.next(undefined);
}
}

importFiles(files: File[]) {
Expand Down Expand Up @@ -127,6 +187,15 @@ export class ImportExportComponent implements OnDestroy, OnInit {
if (data.name !== undefined) {
filename = data.name;
}
const dSOptions: DataSourceOptions = layer.dataSource.options;
if (data.format === ExportFormat.URL && dSOptions.download && dSOptions.download.url) {
setTimeout(() => { // better look an feel
window.open(dSOptions.download.url, '_blank');
this.loading$.next(false);
}, 500);
return;
}

let olFeatures;
if (data.featureInMapExtent) {
olFeatures = layer.dataSource.ol.getFeaturesInExtent(layer.map.viewController.getExtent());
Expand Down Expand Up @@ -208,14 +277,40 @@ export class ImportExportComponent implements OnDestroy, OnInit {
if (this.config.getConfig('importExport.forceNaming') !== undefined) {
this.forceNaming = this.config.getConfig('importExport.forceNaming');
}
this.computeFormats();
}

private computeFormats(layer?: AnyLayer) {
if (layer) {
if (!(layer instanceof VectorLayer) && layer.dataSource.options.download && layer.dataSource.options.download.url) {
// only url key
this.formats$.next(strEnum(['URL']));
} else if (layer.dataSource.options.download && layer.dataSource.options.download.url) {
// add/keep url key
this.computeFormats(); // reset
if (!(ExportFormat.URL in this.formats$.value)) {
const keys = Object.keys(this.formats$.value);
keys.push('URL');
this.formats$.next(strEnum(keys));
}
} else if (layer instanceof VectorLayer) {
// remove url key
this.computeFormats(); // reset
if (ExportFormat.URL in this.formats$.value) {
const keys = Object.keys(this.formats$.value).filter(key => key !== 'URL');
this.formats$.next(strEnum(keys));
}
}
return;
}

if (this.config.getConfig('importExport.formats') !== undefined) {
const validatedListFormat = this.validateListFormat(
this.config.getConfig('importExport.formats')
);
this.formats = strEnum(validatedListFormat);
this.formats$.next(strEnum(validatedListFormat));
} else {
this.formats = ExportFormat;
this.formats$.next(ExportFormat);
}
}

Expand All @@ -228,7 +323,8 @@ export class ImportExportComponent implements OnDestroy, OnInit {
format.toUpperCase() === ExportFormat.GPX.toUpperCase() ||
format.toUpperCase() === ExportFormat.GeoJSON.toUpperCase() ||
format.toUpperCase() === ExportFormat.KML.toUpperCase() ||
format.toUpperCase() === ExportFormat.Shapefile.toUpperCase()
format.toUpperCase() === ExportFormat.Shapefile.toUpperCase() ||
format.toUpperCase() === ExportFormat.URL.toUpperCase()
) {
return format;
}
Expand Down Expand Up @@ -258,6 +354,11 @@ export class ImportExportComponent implements OnDestroy, OnInit {
format = ExportFormat.Shapefile;
return format;
}

if (format.toUpperCase() === ExportFormat.URL.toUpperCase()) {
format = ExportFormat.URL;
return format;
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/geo/src/lib/import-export/shared/export.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { strEnum } from '@igo2/utils';

export const ExportFormat = strEnum(['GeoJSON', 'GML', 'GPX', 'KML', 'Shapefile', 'CSV']);
export const ExportFormat = strEnum(['GeoJSON', 'GML', 'GPX', 'KML', 'Shapefile', 'CSV', 'URL']);
export type ExportFormat = keyof typeof ExportFormat;
2 changes: 2 additions & 0 deletions packages/geo/src/locale/en.geo.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@
},
"importExportForm": {
"exportButton": "Export",
"exportButtonLink": "Open the link",
"exportFormatPlaceholder": "Format",
"exportLayerPlaceholder": "Layer",
"exportFileNamePlaceholder": "Filename",
"exportTabTitle": "Export",
"exportFeatureInExtent": "Export only features in map extent",
"exportNoLayersExportable": "There is no exportable layer in your map",
"importButton": "Import",
"importProjPlaceholder": "Coordinate system",
"importTabTitle": "Import",
Expand Down
2 changes: 2 additions & 0 deletions packages/geo/src/locale/fr.geo.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@
},
"importExportForm": {
"exportButton": "Exporter",
"exportButtonLink": "Ouvrir le lien",
"exportFormatPlaceholder": "Format",
"exportLayerPlaceholder": "Données géospatiales",
"exportFileNamePlaceholder": "Nom du fichier",
"exportTabTitle": "Exporter",
"exportFeatureInExtent": "Seulement les entités contenues dans la carte",
"exportNoLayersExportable": "Aucune couche exportable dans la carte courante",
"importButton": "Importer",
"importProjPlaceholder": "Système de coordonnées",
"importTabTitle": "Importer",
Expand Down
Loading

0 comments on commit f1af5ed

Please sign in to comment.