diff --git a/git-hooks/commit-msg b/git-hooks/commit-msg index 032ee3f60..ace314c05 100755 --- a/git-hooks/commit-msg +++ b/git-hooks/commit-msg @@ -1 +1,2 @@ +#!/bin/sh npx commitlint --edit $1 \ No newline at end of file diff --git a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.html b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.html index ff47fa0ad..682d19be1 100644 --- a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.html +++ b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.html @@ -5,3 +5,14 @@ (catalogSelectChange)="onCatalogSelectChange($event)" > +
+ +
diff --git a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.scss b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.scss new file mode 100644 index 000000000..5ed4ac1f1 --- /dev/null +++ b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.scss @@ -0,0 +1,5 @@ +.get-catalog-list-button { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.ts b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.ts index 53a4ef322..84b306aeb 100644 --- a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.ts +++ b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.component.ts @@ -1,19 +1,45 @@ +import { NgIf, formatDate } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input, + OnDestroy, OnInit } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { EntityStore } from '@igo2/common/entity'; import { ToolComponent } from '@igo2/common/tool'; +import { ContextService, DetailedContext } from '@igo2/context'; +import { LanguageService } from '@igo2/core/language'; import { StorageScope, StorageService } from '@igo2/core/storage'; -import { Catalog, CatalogLibaryComponent, CatalogService } from '@igo2/geo'; +import { + Catalog, + CatalogItem, + CatalogItemGroup, + CatalogItemLayer, + CatalogItemType, + CatalogLibaryComponent, + CatalogService +} from '@igo2/geo'; +import { + addExcelSheetToWorkBook, + createExcelWorkBook, + writeExcelFile +} from '@igo2/utils'; -import { take } from 'rxjs/operators'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable, Subscription, combineLatest, forkJoin } from 'rxjs'; +import { concatAll, map, switchMap, take, toArray } from 'rxjs/operators'; import { ToolState } from '../../tool/tool.state'; import { CatalogState } from '../catalog.state'; +import { + InfoFromSourceOptions, + ListExport +} from './catalog-library-tool.interface'; +import { getInfoFromSourceOptions } from './catalog-library-tool.utils'; /** * Tool to browse the list of available catalogs. @@ -26,11 +52,19 @@ import { CatalogState } from '../catalog.state'; @Component({ selector: 'igo-catalog-library-tool', templateUrl: './catalog-library-tool.component.html', + styleUrls: ['./catalog-library-tool.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CatalogLibaryComponent] + imports: [ + CatalogLibaryComponent, + MatButtonModule, + MatTooltipModule, + NgIf, + TranslateModule + ] }) -export class CatalogLibraryToolComponent implements OnInit { +export class CatalogLibraryToolComponent implements OnInit, OnDestroy { + private generatelist$$: Subscription; /** * Store that contains the catalogs * @internal @@ -44,6 +78,11 @@ export class CatalogLibraryToolComponent implements OnInit { */ @Input() addCatalogAllowed = false; + /** + * Determine if the export button is shown + */ + @Input() exportButton = false; + /** * List of predefined catalogs */ @@ -62,10 +101,12 @@ export class CatalogLibraryToolComponent implements OnInit { } constructor( + private contextService: ContextService, private catalogService: CatalogService, private catalogState: CatalogState, private toolState: ToolState, - private storageService: StorageService + private storageService: StorageService, + private languageService: LanguageService ) {} /** @@ -81,6 +122,10 @@ export class CatalogLibraryToolComponent implements OnInit { } } + ngOnDestroy() { + this.generatelist$$?.unsubscribe(); + } + /** * When the selected catalog changes, toggle the the CatalogBrowser tool. * @internal @@ -110,4 +155,241 @@ export class CatalogLibraryToolComponent implements OnInit { ); }); } + + /** + * Get the item abstract for getCatalogList + */ + private getMetadataAbstract(item: CatalogItemLayer): string { + return item.options.metadata.abstract?.replaceAll('\n', '') ?? ''; + } + /** + * Get the item url metadata for getCatalogList + */ + private getMetadataUrl(item: CatalogItemLayer): string { + return item.options.metadata.url; + } + /** + * Prepare the observale to produce the layer list extraction + * @returns An array of catalog and items plus detailed contexts info. + */ + private getCatalogsAndItemsAndDetailedContexts(): Observable< + [ + { + catalog: Catalog; + items: CatalogItem[]; + }[], + DetailedContext[] + ] + > { + return combineLatest([ + this.store.entities$.pipe( + switchMap((catalogs) => { + return forkJoin( + catalogs.map((catalog) => + this.catalogService.loadCatalogItems(catalog).pipe( + map((items) => { + return { catalog, items }; + }) + ) + ) + ); + }) + ), + this.contextService + .getLocalContexts() + .pipe( + switchMap((contextsList) => + forkJoin( + contextsList.ours.map((context) => + this.contextService.getLocalContext(context.uri) + ) + ) + ) + ) + ]); + } + + private layersInfoFromContexts(): Observable { + return this.contextService.getLocalContexts().pipe( + switchMap((contextsList) => + forkJoin( + contextsList.ours.map((context) => + this.contextService.getLocalContext(context.uri) + ) + ) + ), + concatAll(), + map((detailedContext) => { + return detailedContext.layers.map((layer) => + getInfoFromSourceOptions( + layer.sourceOptions, + detailedContext.title ?? detailedContext.uri + ) + ); + }), + concatAll(), + toArray() + ); + } + + private listExportFromCatalogs(): Observable { + let rank = 1; + const finalListExportOutputs: ListExport[] = []; + return this.store.entities$.pipe( + switchMap((catalogs) => + combineLatest( + catalogs.map((catalog) => + this.catalogService.loadCatalogItems(catalog).pipe( + map((items) => { + return { catalog, items }; + }) + ) + ) + ) + ), + map((catalogsWithItems) => { + catalogsWithItems.forEach((catalogAndItems) => { + const catalog = catalogAndItems.catalog; + const loadedCatalogItems = catalogAndItems.items; + + const catalogListExports = loadedCatalogItems.reduce( + (catalogListExports, item) => { + if (item.type === CatalogItemType.Group) { + const group = item as CatalogItemGroup; + group.items.forEach((layer: CatalogItemLayer) => { + catalogListExports.push( + this.formatLayer(layer, rank, group.title, catalog.title) + ); + rank++; + }); + } else { + const layer = item as CatalogItemLayer; + catalogListExports.push( + this.formatLayer(layer, rank, '', catalog.title) + ); + rank++; + } + return catalogListExports; + }, + [] as ListExport[] + ); + finalListExportOutputs.push(...catalogListExports); + }); + return finalListExportOutputs; + }) + ); + } + + async getCatalogList(): Promise { + this.generatelist$$ = combineLatest([ + this.layersInfoFromContexts(), + this.listExportFromCatalogs() + ]).subscribe(([layersInfoFromContexts, listExportFromCatalogs]) => { + const listExport = this.matchLayersWithLayersFromContext( + listExportFromCatalogs, + layersInfoFromContexts + ); + + this.exportExcel(listExport); + }); + } + + private formatLayer( + layer: CatalogItemLayer, + rank: number, + groupTitle: string, + catalogTitle: string + ): ListExport { + const infos = getInfoFromSourceOptions( + layer.options.sourceOptions, + layer.id + ); + const t = this.languageService.translate; + return { + id: infos.id, + rank: rank.toString(), + layerTitle: layer.title, + layerGroup: groupTitle, + catalog: catalogTitle, + provider: layer.externalProvider + ? t.instant('igo.integration.catalog.listExport.external') + : t.instant('igo.integration.catalog.listExport.internal'), + url: infos.url, + layerName: infos.layerName, + context: '', + metadataAbstract: this.getMetadataAbstract(layer), + metadataUrl: this.getMetadataUrl(layer) + }; + } + + /** + * Match a list of layer info with an other list derived from contexts + * @param catalogOutputs The row list to be written into a file + * @param layerInfosFromDetailedContexts Layers info derived from the context + * @returns An altered list, with layer/context association + */ + + private matchLayersWithLayersFromContext( + listExport: ListExport[], + layerInfosFromDetailedContexts: InfoFromSourceOptions[] + ): ListExport[] { + listExport.map((catalogOutput) => { + const matchingLayersFromContext = layerInfosFromDetailedContexts + .filter( + (l) => + l.id === catalogOutput.id || + (l.layerName === catalogOutput.layerName && + l.url === catalogOutput.url) + ) + .map((f) => f.context); + catalogOutput.context = matchingLayersFromContext.join(','); + }); + return listExport; + } + + /** + * Write a Excel file + * @param catalogOutputs The row list to be written into a excel file + */ + async exportExcel(catalogOutputs: ListExport[]) { + const translateCatalogKey = (key: TemplateStringsArray) => + this.languageService.translate.instant( + `igo.integration.catalog.listExport.${key}` + ); + + catalogOutputs.unshift({ + id: 'catalogIdHeader', + rank: translateCatalogKey`rank`, + layerTitle: translateCatalogKey`layerTitle`, + layerGroup: translateCatalogKey`layerGroup`, + catalog: translateCatalogKey`catalog`, + provider: translateCatalogKey`externalProvider`, + url: translateCatalogKey`url`, + layerName: translateCatalogKey`layerName`, + context: translateCatalogKey`context`, + metadataAbstract: translateCatalogKey`metadataAbstract`, + metadataUrl: translateCatalogKey`metadataUrl` + }); + + const catalogOutput = catalogOutputs.map((catalogOutput) => { + delete catalogOutput.id; + return catalogOutput; + }); + + const workBook = await createExcelWorkBook(); + + await addExcelSheetToWorkBook('Informations', catalogOutput, workBook, { + json2SheetOpts: { + skipHeader: true + } + }); + + const documentName = this.languageService.translate.instant( + 'igo.integration.catalog.listExport.documentName', + { + value: formatDate(Date.now(), 'YYYY-MM-dd-H_mm', 'en-US') + } + ); + writeExcelFile(workBook, documentName, { compression: true }); + } } diff --git a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.interface.ts b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.interface.ts new file mode 100644 index 000000000..8eb386c59 --- /dev/null +++ b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.interface.ts @@ -0,0 +1,23 @@ +import { AnyDataSourceOptions } from '@igo2/geo'; + +export interface ListExport { + id: string; + rank: string; + layerTitle: string; + layerGroup: string; + catalog: string; + provider: string; + url: string; + layerName: string; + context: string; + metadataAbstract: string; + metadataUrl: string; +} + +export interface InfoFromSourceOptions { + id: string; + layerName: string; + url: string; + sourceOptions: AnyDataSourceOptions; + context: string; +} diff --git a/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.utils.ts b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.utils.ts new file mode 100644 index 000000000..ae67d1f15 --- /dev/null +++ b/packages/integration/src/lib/catalog/catalog-library-tool/catalog-library-tool.utils.ts @@ -0,0 +1,120 @@ +import { + AnyDataSourceOptions, + ArcGISRestDataSourceOptions, + CartoDataSourceOptions, + ClusterDataSourceOptions, + FeatureDataSourceOptions, + MVTDataSourceOptions, + OSMDataSourceOptions, + WFSDataSourceOptions, + WMSDataSourceOptions, + WMTSDataSourceOptions, + XYZDataSourceOptions, + generateIdFromSourceOptions +} from '@igo2/geo'; + +import { InfoFromSourceOptions } from './catalog-library-tool.interface'; + +export function getInfoFromSourceOptions( + sourceOptions: AnyDataSourceOptions, + context: string +): InfoFromSourceOptions { + const value: InfoFromSourceOptions = { + id: undefined, + layerName: undefined, + url: undefined, + sourceOptions: undefined, + context + }; + + switch (sourceOptions.type) { + case 'imagearcgisrest': + case 'arcgisrest': + case 'tilearcgisrest': { + const argisSo = sourceOptions as ArcGISRestDataSourceOptions; + value.layerName = argisSo.layer; + value.url = argisSo.url; + value.sourceOptions = argisSo; + break; + } + case 'wmts': { + const wmtsSo = sourceOptions as WMTSDataSourceOptions; + value.layerName = wmtsSo.layer; + value.url = wmtsSo.url; + value.sourceOptions = wmtsSo; + break; + } + case 'xyz': { + const xyzSo = sourceOptions as XYZDataSourceOptions; + value.layerName = ''; + value.url = xyzSo.url; + value.sourceOptions = xyzSo; + break; + } + case 'wms': { + const wmsSo = sourceOptions as WMSDataSourceOptions; + wmsSo.params.LAYERS = wmsSo.params.LAYERS ?? (wmsSo.params as any).layers; + value.layerName = wmsSo.params.LAYERS; + + value.url = wmsSo.url; + value.sourceOptions = wmsSo; + break; + } + case 'osm': { + const osmSo = sourceOptions as OSMDataSourceOptions; + value.layerName = ''; + value.url = osmSo.url + ? osmSo.url + : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + value.sourceOptions = osmSo; + break; + } + case 'wfs': { + const wfsSo = sourceOptions as WFSDataSourceOptions; + value.layerName = wfsSo.params.featureTypes; + value.url = wfsSo.url; + value.sourceOptions = wfsSo; + break; + } + case 'vector': { + const featureSo = sourceOptions as FeatureDataSourceOptions; + value.layerName = ''; + value.url = featureSo.url; + value.sourceOptions = featureSo; + break; + } + case 'cluster': { + const clusterSo = sourceOptions as ClusterDataSourceOptions; + value.layerName = ''; + value.url = clusterSo.url; + value.sourceOptions = clusterSo; + break; + } + case 'mvt': { + const mvtSo = sourceOptions as MVTDataSourceOptions; + value.layerName = ''; + value.url = mvtSo.url; + value.sourceOptions = mvtSo; + break; + } + case 'carto': { + const cartoSo = sourceOptions as CartoDataSourceOptions; + value.layerName = cartoSo.config.layers + .map((layer) => layer.options.sql) + .join(' '); + value.url = `https://${cartoSo.account}.carto.com/api/v1/map`; + value.sourceOptions = cartoSo; + break; + } + default: + break; + } + if (value.sourceOptions) { + value.id = generateIdFromSourceOptions(value.sourceOptions); + value.url = value.url?.startsWith('/') + ? window.location.origin + value.url + : value.url; + } + + return value; +} diff --git a/packages/integration/src/locale/en.integration.json b/packages/integration/src/locale/en.integration.json index 27db49428..64b636389 100644 --- a/packages/integration/src/locale/en.integration.json +++ b/packages/integration/src/locale/en.integration.json @@ -22,6 +22,24 @@ "advancedMap": "Advanced map tools", "closestFeature": "Closest feature tool" }, + "catalog": { + "library.getCatalogList": "Get the catalogs's content list", + "listExport": { + "rank": "Rank", + "layerTitle": "Layer Title", + "layerGroup": "Layer Group", + "catalog": "Catalog", + "externalProvider": "Provider", + "url": "URL", + "layerName": "Layer name", + "context": "Context/Thematic", + "metadataAbstract": "Data description", + "metadataUrl": "Metadata hyperlink", + "documentName": "LayerList_{{value}}", + "internal": "Internal", + "external": "External" + } + }, "dataIssueReporterTool": { "submit": { "title": "Submit a data inconsistency", diff --git a/packages/integration/src/locale/fr.integration.json b/packages/integration/src/locale/fr.integration.json index c869c11bd..710102d22 100644 --- a/packages/integration/src/locale/fr.integration.json +++ b/packages/integration/src/locale/fr.integration.json @@ -22,6 +22,25 @@ "advancedMap": "Outils avancés", "closestFeature": "Entités à proximité" }, + "catalog": { + "library.getCatalogList": "Obtenir la liste du contenu des catalogues", + "listExport": { + "rank": "Rang", + "layerTitle": "Titre de la couche", + "layerGroup": "Nom du groupe de couches", + "catalog": "Catalogue", + "externalProvider": "Fournisseur", + "url": "URL", + "layerName": "Nom système de la couche d'informaiton", + "context": "Contexte/Thématique", + "dataDescription": "Description de la donnée", + "metadataAbstract": "Description de la donnée", + "metadataUrl": "Lien des métadonnées", + "documentName": "ListeCouchesInformation_{{value}}", + "internal": "Interne", + "external": "Externe" + } + }, "dataIssueReporterTool": { "submit": { "title": "Soumettre une incohérence de donnée",