Source AddData
Source AddData
Cette page est générée automatiquement à partir du dépôt local au moment de la génération de la documentation.
Fichiers inclus
packages/common/src/lib/widgets/add-data/add-data.declaration.tspackages/common/src/lib/widgets/add-data/add-data.config.tspackages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.config.tspackages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.declaration.tspackages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.i18n.tspackages/common/src/lib/widgets/add-data/add-data-advanced-options/AddDataAdvancedOptions.sveltepackages/common/src/lib/widgets/add-data/add-data-advanced-options/AddDataAdvancedOptionsWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog-state.svelte.tspackages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.config.tspackages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.declaration.tspackages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.i18n.tspackages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalog.sveltepackages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalogServiceList.sveltepackages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalogWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-categories.translations.tspackages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataCategoryTree.sveltepackages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataCswRecordList.sveltepackages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataFilterByCategory.sveltepackages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataTimeTravelSeries.sveltepackages/common/src/lib/widgets/add-data/add-data-from-categories/RootCategoryList.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.config.tspackages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.declaration.tspackages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n.tspackages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.model.tspackages/common/src/lib/widgets/add-data/add-data-from-file/AddDataFromFile.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/AddDataFromFileWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/CSVFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GeoJSONFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GMLFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GPXFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/KMLFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/KMZFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/ShapeFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte.tspackages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/SpatialFileInput.sveltepackages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url-widget.config.tspackages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.declaration.tspackages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.i18n.tspackages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.model.tspackages/common/src/lib/widgets/add-data/add-data-from-url/AddDataFromUrl.sveltepackages/common/src/lib/widgets/add-data/add-data-from-url/AddDataFromUrlWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-from-url/LayerHierarchySelect.sveltepackages/common/src/lib/widgets/add-data/add-data-from-url/tree-item.svelte.tspackages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.config.tspackages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.declaration.tspackages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.i18n.tspackages/common/src/lib/widgets/add-data/add-data-populars/AddDataPopulars.sveltepackages/common/src/lib/widgets/add-data/add-data-populars/AddDataPopularsWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.config.tspackages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.declaration.tspackages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.i18n.tspackages/common/src/lib/widgets/add-data/add-data-presets/AddDataPresets.sveltepackages/common/src/lib/widgets/add-data/add-data-presets/AddDataPresetsWidget.sveltepackages/common/src/lib/widgets/add-data/add-data-search/add-data-search.config.tspackages/common/src/lib/widgets/add-data/add-data-search/add-data-search.model.tspackages/common/src/lib/widgets/add-data/add-data-search/AddDataSearch.sveltepackages/common/src/lib/widgets/add-data/add-data-search/AddDataSearchResults.sveltepackages/common/src/lib/widgets/add-data/add-data.i18n.tspackages/common/src/lib/widgets/add-data/add-data.svelte.tspackages/common/src/lib/widgets/add-data/add-data.topic.tspackages/common/src/lib/widgets/add-data/AddData.sveltepackages/common/src/lib/widgets/add-data/card-list/AddDataBackButton.sveltepackages/common/src/lib/widgets/add-data/card-list/AddDataBottomButtons.sveltepackages/common/src/lib/widgets/add-data/card-list/card-list.model.tspackages/common/src/lib/widgets/add-data/card-list/CardItem.sveltepackages/common/src/lib/widgets/add-data/card-list/CardList.sveltepackages/common/src/lib/widgets/add-data/default-categories.tspackages/common/src/lib/widgets/add-data/default-timetravel-series.ts
packages/common/src/lib/widgets/add-data/add-data.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { type AddDataConfig, addDataConfigSchema } from './add-data.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddData.svelte').then((AddData) => widgetFactorySvelte(AddData)), schema: () => addDataConfigSchema,} satisfies WidgetDeclaration;
export type AddDataProps = WidgetProps<AddDataConfig>;packages/common/src/lib/widgets/add-data/add-data.config.ts
import { type Icon, iconSchema } from '$lib/api/icons';import { type I18nData, i18nDataSchema, i18nSchemaFrom } from '$lib/api/managers/i18n';import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { DefaultMetawalApiFilterBySubjectQuery, DefaultMetawalFilterByTitleQuery, metawalApiFilterBySubjectQuerySchema, metawalApiFilterByTitleQueryParamsSchema,} from '$lib/api/tools/query';import { addDataSearchConfigSchema } from '$lib/widgets/add-data/add-data-search/add-data-search.config';import { DEFAULT_ADD_DATA_CATEGORIES } from '$lib/widgets/add-data/default-categories';import { z } from 'zod';import { addDataI18n } from '$lib/widgets/add-data/add-data.i18n';import { addDataAdvancedOptionsConfigSchema } from '$lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.config';import { addDataPopularsConfigSchema } from '$lib/widgets/add-data/add-data-populars/add-data-populars.config';import { type TimeTravelMapServiceConfiguration, timeTravelMapServiceConfigurationSchema,} from '$lib/api/managers/configuration';import { DEFAULT_TIME_TRAVEL_SERIES } from '$lib/widgets/add-data/default-timetravel-series';import { addDataCatalogConfigSchema } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.config';
export type AddDataCategory = { code: string; label: I18nData; icon: Icon; categories?: AddDataCategory[]; timeTravelConfigs?: TimeTravelMapServiceConfiguration[];};
export const addDataCategorySchema = z.lazy(() => z.object({ code: z.string(), label: i18nDataSchema, icon: iconSchema, categories: z.array(addDataCategorySchema).optional(), timeTravelConfigs: z.array(timeTravelMapServiceConfigurationSchema).optional(), }),) as z.Schema<AddDataCategory>;
export const addDataMetawalApiParamsSchema = z.object({ filterBySubjectQueryParams: metawalApiFilterBySubjectQuerySchema .optional() .default(DefaultMetawalApiFilterBySubjectQuery), filterByTitleQueryParams: metawalApiFilterByTitleQueryParamsSchema .optional() .default(DefaultMetawalFilterByTitleQuery),});export type AddDataMetawalApiParams = z.infer<typeof addDataMetawalApiParamsSchema>;
export const addDataConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataI18n), title: addDataI18n['add-data-title'], icon: { geoviewer: 'add-layer', }, inToolbar: inToolbarSchemaFrom({ type: 'button', }), container: { id: 'dialog', draggable: true, width: '40vw', minWidth: '650px', maxHeight: '85vh', y: '30px', center: true, }, onActivate: { deactivate: { classes: ['Draw', 'MeasureSurface', 'MeasureDistance', 'Export', 'Report', 'AdvancedSearch'], }, }, config: z .object({ addDataPopularsConfig: addDataPopularsConfigSchema, addDataAdvancedOptionsConfig: addDataAdvancedOptionsConfigSchema, addDataCatalogConfig: addDataCatalogConfigSchema, addDataSearchConfig: addDataSearchConfigSchema, categories: z.array(addDataCategorySchema).optional().default(DEFAULT_ADD_DATA_CATEGORIES), timeTravelSeries: z.array(addDataCategorySchema).optional().default(DEFAULT_TIME_TRAVEL_SERIES), metawalConfig: addDataMetawalApiParamsSchema.optional().default(addDataMetawalApiParamsSchema.parse({})), closeOnMapServiceAdded: z.boolean().default(true), openTocOnMapServiceAdded: z.boolean().default(true), visibleTabs: z .object({ populars: z.boolean().default(true), categories: z.boolean().default(true), predefinedViews: z.boolean().default(true), advancedOptions: z.boolean().default(true), catalog: z.boolean().default(false), }) .prefault({}), }) .prefault({}),});
export type AddDataConfig = z.infer<typeof addDataConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { z } from 'zod';import { addDataAdvancedOptionsI18n } from '$lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.i18n';import { addDataFromFileConfigSchema } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.config';import { addDataFromUrlConfigSchema } from '$lib/widgets/add-data/add-data-from-url/add-data-from-url-widget.config';
export const fileOptionEnumSchema = z.enum(['URL', 'FILE']);
export const addDataAdvancedOptionsConfigSchema = z .object({ addDataFromFileConfig: addDataFromFileConfigSchema, addDataFromUrlConfig: addDataFromUrlConfigSchema, visibleOptions: z.array(fileOptionEnumSchema).default(['URL', 'FILE']), selectedOption: fileOptionEnumSchema.optional(), }) .prefault({});export type AddDataAdvancedOptionsConfig = z.infer<typeof addDataAdvancedOptionsConfigSchema>;
export const addDataAdvancedOptionsWidgetSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataAdvancedOptionsI18n), title: addDataAdvancedOptionsI18n['add-data-advanced-options-title'], inToolbar: inToolbarSchemaFrom(false), config: addDataAdvancedOptionsConfigSchema,});
export type AddDataAdvancedOptionsWidget = z.infer<typeof addDataAdvancedOptionsWidgetSchema>;packages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import type { WidgetInitializer } from '$lib/api/managers/widget/widget-registry';import { type AddDataAdvancedOptionsWidget, addDataAdvancedOptionsConfigSchema,} from './add-data-advanced-options.config';
export const declaration = { factory: () => import('./AddDataAdvancedOptionsWidget.svelte').then((AddDataAdvancedOptionsWidget) => widgetFactorySvelte(AddDataAdvancedOptionsWidget), ), schema: () => addDataAdvancedOptionsConfigSchema,} satisfies WidgetInitializer;
export type AddDataAdvancedOptionsProps = WidgetProps<AddDataAdvancedOptionsWidget>;packages/common/src/lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';import { addDataFromUrlI18n } from '$lib/widgets/add-data/add-data-from-url/add-data-from-url.i18n';import { addDataFromFileI18n } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n';
export const addDataAdvancedOptionsI18n = { 'add-data-advanced-options-title': { fr: 'Options avancées', nl: 'NL - Options avancées', }, 'advanced-options-url': { fr: "Depuis l'URL d'un service", nl: "NL - Depuis l'URL d'un service", }, 'advanced-options-file': { fr: 'Depuis un fichier', nl: 'NL - Depuis un fichier', }, ...addDataFromUrlI18n, ...addDataFromFileI18n,} satisfies I18nRegistry;
export type AddDataAdvancedOptionsI18n = I18nRegistry<keyof typeof addDataAdvancedOptionsI18n>;packages/common/src/lib/widgets/add-data/add-data-advanced-options/AddDataAdvancedOptions.svelte
<script lang="ts"> import { getI18n } from '$lib/api/managers/i18n/i18n.manager.svelte'; import AddDataFromUrl from '$lib/widgets/add-data/add-data-from-url/AddDataFromUrl.svelte'; import AddDataFromFile from '$lib/widgets/add-data/add-data-from-file/AddDataFromFile.svelte'; import type { ComponentItemListOption } from '$lib/components/component-item-list/component-item-list.model.js'; import type { AddDataAdvancedOptionsConfig } from '$lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.config'; import type { AddDataAdvancedOptionsI18n } from '$lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.i18n'; import { ChevronLeft } from 'lucide-svelte'; import ComponentItemList from '$lib/components/component-item-list/ComponentItemList.svelte';
interface Props { config: AddDataAdvancedOptionsConfig; i18nRegistry: AddDataAdvancedOptionsI18n; }
let { config, i18nRegistry }: Props = $props();
const i18n = getI18n(i18nRegistry);
let selectedItem = $state<ComponentItemListOption | undefined>();
const addDataFromUrlOption: ComponentItemListOption<typeof AddDataFromUrl> = { label: i18n('advanced-options-url'), code: 'URL', icon: { lucide: 'Link' }, component: AddDataFromUrl, props: { config: config.addDataFromUrlConfig, i18nRegistry: i18nRegistry }, };
const addDataFromFileOption: ComponentItemListOption<typeof AddDataFromFile> = { label: i18n('advanced-options-file'), code: 'FILE', icon: { lucide: 'File' }, component: AddDataFromFile, props: { config: config.addDataFromFileConfig, i18nRegistry: i18nRegistry }, };
const advancedOptions: ComponentItemListOption[] = $derived.by(() => { const options = []; if (config.visibleOptions.indexOf('URL') > -1) { options.push(addDataFromUrlOption); } if (config.visibleOptions.indexOf('FILE') > -1) { options.push(addDataFromFileOption); } return options; });
$effect(() => { if (config.selectedOption) { selectedItem = advancedOptions.find((x) => x.code === config.selectedOption); } });
function onBack() { selectedItem = undefined; }</script>
{#if selectedItem} <button class="gv-flex gv-font-bold -gv-ml-1" onclick={onBack} data-test-id="AddData-BackButton"> <ChevronLeft class="gv-size-5 gv-text-primary" /> <span>{i18n('common.back')}</span> </button> <selectedItem.component {...selectedItem.props} />{:else} <ComponentItemList options={advancedOptions} onItemClick={(item) => (selectedItem = item)} />{/if}packages/common/src/lib/widgets/add-data/add-data-advanced-options/AddDataAdvancedOptionsWidget.svelte
<script lang="ts"> import AddDataAdvancedOptions from './AddDataAdvancedOptions.svelte'; import type { AddDataAdvancedOptionsProps } from './add-data-advanced-options.declaration';
let { fullConfig }: AddDataAdvancedOptionsProps = $props();</script>
<AddDataAdvancedOptions config={fullConfig.config} i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog-state.svelte.ts
import { type MapServiceConfiguration, mapServiceConfigWithDefaults } from '$lib/api/managers/configuration';import { getContext, setContext } from 'svelte';import { highlightServiceInToc } from '$lib/widgets/toc/toc.utils';import { type MapManager } from '$lib/api/map';import type { AddDataConfig } from '$lib/widgets/add-data/add-data.config';import type WidgetManager from '$lib/api/managers/widget/widget.manager.svelte';import type { TopicManager } from '$lib/api/managers/topic';
export class AddDataCatalogState { private _selectedMapServiceConfigs = $state<MapServiceConfiguration[]>([]); public selectionCount = $derived.by(() => this._selectedMapServiceConfigs.length);
private closeWidgetOnServiceAdded = $derived.by(() => this.addDataFullConfig.config.closeOnMapServiceAdded); private openTocOnServiceAdded = $derived.by(() => this.addDataFullConfig.config.openTocOnMapServiceAdded);
constructor( private mapManager: MapManager, private widgetManager: WidgetManager, private addDataFullConfig: AddDataConfig, private topicManager: TopicManager, ) {}
public toggleRecord(config: MapServiceConfiguration, toggle: boolean): void { if (toggle) { this._selectedMapServiceConfigs.push(config); } else { this._selectedMapServiceConfigs = this._selectedMapServiceConfigs.filter((x) => x.id !== config.id); } }
public emptySelection() { this._selectedMapServiceConfigs = []; }
public addSelectedData() { this._selectedMapServiceConfigs.forEach((config) => { try { const mapServiceConfig = mapServiceConfigWithDefaults({ ...config, }); const mapService = this.mapManager.addMapService(mapServiceConfig); this.topicManager.publish({ type: 'AddData-add-mapService-from-catalog', layer: mapService, }); if (mapService && this.openTocOnServiceAdded) { highlightServiceInToc(mapService.id, this.widgetManager, this.mapManager); } } catch (error) { console.log(`Error while adding map service ${config.id} on map`, error); } }); this.emptySelection(); if (this.closeWidgetOnServiceAdded) { this.closeWidget(); } }
public closeWidget() { const reference = this.widgetManager.getReference(this.addDataFullConfig.widgetId); reference.deactivate(); }
public get selectedMapServiceConfigs(): MapServiceConfiguration[] { return this._selectedMapServiceConfigs; }}
const ADD_DATA_CATALOG_CONTEXT_KEY = 'ADD_DATA_CATALOG_CONTEXT_KEY';
export function setAddDataCatalogContext(addData: AddDataCatalogState) { setContext(ADD_DATA_CATALOG_CONTEXT_KEY, addData); return getAddDataCatalog();}
export function getAddDataCatalog(): AddDataCatalogState { const addData = getContext<AddDataCatalogState>(ADD_DATA_CATALOG_CONTEXT_KEY); if (!addData) { throw new Error('AddDataCatalog not found in context.'); } return addData;}packages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { type I18nData, i18nDataSchema, i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { z } from 'zod';import { addDataPresetsI18n } from '$lib/widgets/add-data/add-data-presets/add-data-presets.i18n';import { addDataCatalogI18n } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.i18n';import { type Icon, iconSchema } from '$lib/api/icons';import { type MapServiceConfiguration } from '$lib/api/managers/configuration';
export type AddDataCatalog = { code: string; label: I18nData; icon: Icon; mapServiceConfigs?: MapServiceConfiguration[];};
export const catalogConfigSchema = z.lazy(() => z.object({ code: z.string(), label: i18nDataSchema, icon: iconSchema, mapServiceConfigs: z.array(z.custom<MapServiceConfiguration>()).optional().default([]), }),) as z.Schema<AddDataCatalog>;
export const addDataCatalogConfigSchema = z .object({ catalogs: z.array(catalogConfigSchema).default([]), }) .prefault({});export type AddDataCatalogConfig = z.infer<typeof addDataCatalogConfigSchema>;
export const addDataCatalogWidgetConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataPresetsI18n), title: addDataCatalogI18n['add-data-catalog-title'], inToolbar: inToolbarSchemaFrom(false),});
export type AddDataCatalogWidgetConfig = z.infer<typeof addDataCatalogWidgetConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { type AddDataCatalogWidgetConfig, addDataCatalogWidgetConfigSchema } from './add-data-catalog.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddDataCatalogWidget.svelte').then((AddDataCatalogWidget) => widgetFactorySvelte(AddDataCatalogWidget), ), schema: () => addDataCatalogWidgetConfigSchema,} satisfies WidgetDeclaration;
export type AddDataCatalogProps = WidgetProps<AddDataCatalogWidgetConfig>;packages/common/src/lib/widgets/add-data/add-data-catalog/add-data-catalog.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addDataCatalogI18n = { 'add-data-catalog-title': { fr: 'Catalogue de données', nl: 'NL - Catalogue de données', }, ADD: { fr: 'Ajouter les données sélectionnées', nl: 'NL - Ajouter les données sélectionnées', }, EMPTY: { fr: 'Vider', nl: 'NL - Vider', },} satisfies I18nRegistry;
export type AddDataCatalogI18n = I18nRegistry<keyof typeof addDataCatalogI18n>;packages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalog.svelte
<script lang="ts"> import { type AddDataCatalog } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.config'; import type { AddDataCatalogI18n } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.i18n'; import { getI18n } from '$lib/api/managers/i18n'; import ComponentItemList from '$lib/components/component-item-list/ComponentItemList.svelte'; import AddDataBackButton from '$lib/widgets/add-data/card-list/AddDataBackButton.svelte'; import AddDataCatalogServiceList from '$lib/widgets/add-data/add-data-catalog/AddDataCatalogServiceList.svelte'; import { AddDataCatalogState, setAddDataCatalogContext, } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog-state.svelte'; import AddDataBottomButtons from '$lib/widgets/add-data/card-list/AddDataBottomButtons.svelte'; import { getMapManager } from '$lib/api/map'; import { getWidgetManager } from '$lib/api/managers/widget'; import type { AddDataConfig } from '$lib/widgets/add-data/add-data.config'; import { getTopicManager } from '$lib/api/managers/topic';
interface Props { addDataConfig: AddDataConfig; i18nRegistry: AddDataCatalogI18n; }
let { addDataConfig, i18nRegistry }: Props = $props(); const { addDataCatalogConfig } = addDataConfig.config; const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const widgetManager = getWidgetManager(); const topicManager = getTopicManager(); const addDataContext = setAddDataCatalogContext( new AddDataCatalogState(mapManager, widgetManager, addDataConfig, topicManager), );
let currentCatalog = $state<AddDataCatalog | undefined>();</script>
{#if !currentCatalog} <div class="gv-max-h-[55vh] gv-overflow-y-auto gv-flex gv-flex-col"> <ComponentItemList options={addDataCatalogConfig.catalogs} onItemClick={(option) => (currentCatalog = option)} i18nData={i18nRegistry} /> </div>{:else} <AddDataBackButton {i18nRegistry} onclick={() => (currentCatalog = undefined)} /> <div class="gv-pb-3 gv-pt-2 gv-font-bold gv-text-2xl">{i18n.translate(currentCatalog.label)}</div> <div class="gv-max-h-[55vh] gv-overflow-y-auto gv-flex gv-flex-col"> <AddDataCatalogServiceList catalog={currentCatalog} {i18nRegistry} /> </div>
<div class="gv-pt-5"> <AddDataBottomButtons selectionCount={addDataContext.selectionCount} emptyButtonLabel={i18n('EMPTY')} addButtonLabel={i18n('ADD')} onAddClicked={() => addDataContext.addSelectedData()} onEmptyClicked={() => addDataContext.emptySelection()} /> </div>{/if}packages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalogServiceList.svelte
<script lang="ts"> import { type AddDataCatalog } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.config'; import type { AddDataCatalogI18n } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.i18n'; import { getI18n } from '$lib/api/managers/i18n'; import NoResult from '$lib/components/common/NoResult.svelte'; import type { MapServiceConfiguration } from '$lib/api/managers/configuration'; import { getMapManager } from '$lib/api/map'; import { cn } from '$lib/components/shadcn/utils'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { getAddDataCatalog } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog-state.svelte';
interface Props { catalog: AddDataCatalog; i18nRegistry: AddDataCatalogI18n; }
let { catalog, i18nRegistry }: Props = $props();
const addDataContext = getAddDataCatalog(); const mapManager = getMapManager(); const i18n = getI18n(i18nRegistry); const mapServiceConfigs = $derived.by(() => catalog.mapServiceConfigs ?? []); let checkboxRefs = $state<Checkbox[]>([]);
$effect(() => { const selectedIds = addDataContext.selectedMapServiceConfigs.map((serviceConfig) => serviceConfig.id); mapServiceConfigs.forEach((mapServiceConfig, index) => { if (!mapServiceConfig.id) { return; } const checkbox = checkboxRefs[index]; if (checkbox) { checkbox.checked = selectedIds.indexOf(mapServiceConfig.id) > -1; } }); });
function toggleIndex(config: MapServiceConfiguration, index: number) { const checkbox = checkboxRefs[index]; const active = checkbox.checked; checkbox.checked = !active; addDataContext.toggleRecord(config, !active); }
function mapServiceAlreadyOnMap(id: string) { return !!mapManager.layerList.list.find((layer) => layer.id === id); }</script>
<div class="gv-w-full gv-h-full gv-overflow-y-auto"> {#if !catalog || !catalog.mapServiceConfigs || catalog.mapServiceConfigs?.length === 0} <NoResult /> {/if} {#each mapServiceConfigs as mapServiceConfig, index} {@const alreadyOnMap = mapServiceAlreadyOnMap(mapServiceConfig.id)} <div title={alreadyOnMap ? i18n('common.already-on-map') : i18n('common.add-xxx-to-map', { label: mapServiceConfig.label })} class={cn( 'gv-flex gv-w-full gv-items-center gv-p-2 gv-border-b-[0.5px] gv-border-grey-300', alreadyOnMap ? '' : 'hover:gv-bg-primary/10', )} > <div class="gv-w-[5%] gv-mt-[1px] gv-flex gv-items-center"> {#if alreadyOnMap} <Checkbox title={i18n('common.select-data')} disabled={true} checked={true} data-test-id={`AddData-Catalog-${mapServiceConfig.label}-Checkbox`} /> {:else} <Checkbox title={i18n('common.select-data')} data-test-id={`AddData-${mapServiceConfig.label}-Checkbox`} bind:this={checkboxRefs[index]} onCheckedChange={() => toggleIndex(mapServiceConfig, index)} /> {/if} </div> <div class="gv-w-[95%] gv-flex gv-justify-start"> <button class={cn('gv-w-full gv-text-left', alreadyOnMap ? 'gv-opacity-50' : '')} onclick={() => !alreadyOnMap && toggleIndex(mapServiceConfig, index)} > {mapServiceConfig.label} </button> </div> </div> {/each}</div>packages/common/src/lib/widgets/add-data/add-data-catalog/AddDataCatalogWidget.svelte
<script lang="ts"> import AddDataCatalog from '$lib/widgets/add-data/add-data-catalog/AddDataCatalog.svelte'; import type { AddDataCatalogProps } from '$lib/widgets/add-data/add-data-catalog/add-data-catalog.declaration';
let { fullConfig }: AddDataCatalogProps = $props();</script>
<AddDataCatalog i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-categories.translations.ts
export const addDataCategoriesTranslations = { 'Nature et environnement': { fr: 'Nature et environnement', nl: 'NL - Nature et environnement', }, 'Aménagement du territoire': { fr: 'Aménagement du territoire', nl: 'NL - Aménagement du territoire', }, Mobilité: { fr: 'Mobilité', nl: 'NL - Mobilité', }, 'Tourisme et loisirs': { fr: 'Tourisme et loisirs', nl: 'NL - Tourisme et loisirs', }, 'Données de base': { fr: 'Données de base', nl: 'NL - Données de base', }, 'Société et activité': { fr: 'Société et activité', nl: 'NL - Société et activité', }, 'Faune et flore': { fr: 'Faune et flore', nl: 'NL - Faune et flore', }, Eau: { fr: 'Eau', nl: 'NL - Eau', }, 'Sol et sous-sol': { fr: 'Sol et sous-sol', nl: 'NL - Sol et sous-sol', }, Air: { fr: 'Air', nl: 'NL - Air', }, Autres: { fr: 'Autres', nl: 'NL - Autres', }, 'Plans et règlements': { fr: 'Plans et règlements', nl: 'NL - Plans et règlements', }, 'Risques et contraintes': { fr: 'Risques et contraintes', nl: 'NL - Risques et contraintes', }, Routes: { fr: 'Routes', nl: 'NL - Routes', }, 'A pied et à vélo': { fr: 'A pied et à vélo', nl: 'NL - A pied et à vélo', }, 'Voies navigables': { fr: 'Voies navigables', nl: 'NL - Voies navigables', }, 'Transports en commun': { fr: 'Transports en commun', nl: 'NL - Transports en commun', }, Tourisme: { fr: 'Tourisme', nl: 'NL - Tourisme', }, Loisirs: { fr: 'Loisirs', nl: 'NL - Loisirs', }, 'Données topographiques': { fr: 'Données topographiques', nl: 'NL - Données topographiques', }, 'Limites administratives': { fr: 'Limites administratives', nl: 'NL - Limites administratives', }, 'Photos et imageries': { fr: 'Photos et imageries', nl: 'NL - Photos et imageries', }, 'Cartes anciennes': { fr: 'Cartes anciennes', nl: 'NL - Cartes anciennes', }, 'Industrie et services': { fr: 'Industrie et services', nl: 'NL - Industrie et services', }, Agriculture: { fr: 'Agriculture', nl: 'NL - Agriculture', }, 'Logement et habitat': { fr: 'Logement et habitat', nl: 'NL - Logement et habitat', }, Bruit: { fr: 'Bruit', nl: 'NL - Bruit', },};packages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataCategoryTree.svelte
<script lang="ts"> import { getI18n } from '$lib/api/managers/i18n/i18n.manager.svelte'; import type { I18nRegistry } from '$lib/api/managers/i18n/i18n.schema'; import AddDataFilterByCategory from '$lib/widgets/add-data/add-data-from-categories/AddDataFilterByCategory.svelte'; import AddDataCategoryTree from './AddDataCategoryTree.svelte'; import type { ComponentItemListOption } from '$lib/components/component-item-list/component-item-list.model'; import AddDataBackButton from '$lib/widgets/add-data/card-list/AddDataBackButton.svelte'; import ComponentItemList from '$lib/components/component-item-list/ComponentItemList.svelte';
type Props = { parentOption: ComponentItemListOption; onOptionClick: (option: ComponentItemListOption | undefined) => void; i18nRegistry: I18nRegistry; }; let { parentOption, onOptionClick, i18nRegistry }: Props = $props();
let selectedSubCategory = $state<ComponentItemListOption | undefined>();
const i18n = getI18n(i18nRegistry ?? {});
function onSubCategoryClicked(option: ComponentItemListOption) { selectedSubCategory = option; }
function onBackFromMapServicesList() { if (selectedSubCategory) { onOptionClick(parentOption); selectedSubCategory = undefined; } else { onOptionClick(undefined); } }</script>
{#if parentOption.categories && !selectedSubCategory} <AddDataBackButton class="gv-ml-[-6px] gv-mt-6" {i18nRegistry} onclick={() => onOptionClick(undefined)} /> <div class="gv-pb-3 gv-pt-2 gv-font-bold gv-text-2xl">{i18n.translate(parentOption.label)}</div> <div class="gv-h-full"> <ComponentItemList options={parentOption.categories} onItemClick={(option) => onSubCategoryClicked(option)} i18nData={i18nRegistry} /> </div>{:else if selectedSubCategory && selectedSubCategory.categories} <AddDataBackButton {i18nRegistry} onclick={() => onOptionClick(parentOption)} /> <AddDataCategoryTree {i18nRegistry} {onOptionClick} parentOption={selectedSubCategory} />{:else if selectedSubCategory} <div class="gv-pb-2 gv-text-sm gv-opacity-70"> <button class="gv-text-foreground/80 hover:gv-text-foreground" onclick={onBackFromMapServicesList}> {i18n.translate(parentOption.label)} </button> > {i18n.translate(selectedSubCategory.label)} </div> <AddDataBackButton class="gv-ml-[-6px]" {i18nRegistry} onclick={onBackFromMapServicesList} /> <div class="gv-pb-3 gv-pt-2 gv-font-bold gv-text-2xl">{i18n.translate(selectedSubCategory.label)}</div> <AddDataFilterByCategory category={parentOption} subCategory={selectedSubCategory} />{/if}packages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataCswRecordList.svelte
<script lang="ts"> import type { ApiCswRecord } from '$lib/api/clients'; import Loader from '$lib/components/common/Loader.svelte'; import NoResult from '$lib/components/common/NoResult.svelte'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import CSWResultDisplay from '$lib/widgets/global-search/csw-display/CSWResultDisplay.svelte'; import { getMapManager } from '$lib/api/map'; import { cn } from '$lib/components/shadcn/utils'; import { getI18n } from '$lib/api/managers/i18n'; import type { AddDataCategory } from '$lib/widgets/add-data/add-data.config'; interface Props { showIcon?: boolean; subCategory?: AddDataCategory; category?: AddDataCategory; }
let { showIcon = true, category, subCategory }: Props = $props();
const addDataContext = getAddData(); const mapManager = getMapManager(); const layerList = mapManager.layerList; const i18n = getI18n();
let checkboxRefs = $state<Checkbox[]>([]);
function mapServiceAlreadyOnMap(identifier: string | undefined): boolean { if (!identifier) return false; return !!layerList.list.find((x) => x.id === identifier); }
$effect(() => { const selectedIdsForCategory = addDataContext.selectedCswRecords.map((f) => f.record.identifier); addDataContext.sortedResults.forEach((feature, index) => { if (!feature.identifier) { return; } const checkbox = checkboxRefs[index]; if (checkbox) { checkbox.checked = selectedIdsForCategory.indexOf(feature.identifier) > -1; } }); });
function onInfoIconClick(event: MouseEvent, result: ApiCswRecord) { event.stopPropagation(); const id = result.identifier; if (id) { addDataContext.currentInfoOpened = id; } }
function toggleIndex(cswRecord: ApiCswRecord, index: number) { const checkbox = checkboxRefs[index]; const active = checkbox.checked; checkbox.checked = !active; addDataContext.toggleRecord( { record: cswRecord, category: subCategory?.code, topCategory: category?.code, }, !active, ); }</script>
<div class="gv-w-full gv-h-full gv-overflow-y-auto" data-add-data-csw-record-list> {#if addDataContext.loading} <div class="gv-p-3"> <Loader /> </div> {:else} {#if !addDataContext.sortedResults || addDataContext.sortedResults.length === 0} <NoResult /> {/if} {#each addDataContext.sortedResults as cswRecord, index} {@const alreadyOnMap = mapServiceAlreadyOnMap(cswRecord.identifier)} <div title={alreadyOnMap ? i18n('common.already-on-map') : i18n('common.add-xxx-to-map', { label: cswRecord.title })} class={cn( 'gv-flex gv-w-full gv-items-center gv-p-2 gv-border-b-[0.5px] gv-border-grey-300', alreadyOnMap ? '' : 'hover:gv-bg-primary/10', )} > <div class="gv-w-[5%] gv-mt-[1px] gv-flex gv-items-center"> {#if alreadyOnMap} <Checkbox title={i18n('common.select-data')} disabled={true} checked={true} data-test-id={`AddData-${cswRecord.title}-Checkbox`} /> {:else} <Checkbox title={i18n('common.select-data')} data-test-id={`AddData-${cswRecord.title}-Checkbox`} bind:this={checkboxRefs[index]} onCheckedChange={() => toggleIndex(cswRecord, index)} /> {/if} </div> <div class="gv-w-[95%] gv-flex gv-items-center"> <button class={cn('gv-flex gv-w-full', alreadyOnMap ? 'gv-opacity-50' : '')} onclick={() => !alreadyOnMap && toggleIndex(cswRecord, index)} > <div class="gv-w-full"> <CSWResultDisplay {showIcon} showIconTooltip={i18n('common.show-data-description')} closeIconTooltip={i18n('common.close-data-description')} onInfoIconClick={(e) => onInfoIconClick(e, cswRecord)} infoVisible={addDataContext.currentInfoOpened === cswRecord.identifier} detailsMode={true} addToMapOnClick={false} {cswRecord} displayField={addDataContext.cswRecordDisplayField} dataTestId={`AddData-${cswRecord.title}`} /> </div> </button> </div> </div> {/each} {/if}</div>packages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataFilterByCategory.svelte
<script lang="ts"> import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import type { AddDataCategory } from '$lib/widgets/add-data/add-data.config'; import AddDataCswRecordList from '$lib/widgets/add-data/add-data-from-categories/AddDataCswRecordList.svelte';
type Props = { subCategory: AddDataCategory; category: AddDataCategory; }; let { subCategory, category }: Props = $props();
const addDataContext = getAddData(); addDataContext.searchRecordsByCategory(category, subCategory);</script>
<AddDataCswRecordList showIcon={false} {category} {subCategory} />packages/common/src/lib/widgets/add-data/add-data-from-categories/AddDataTimeTravelSeries.svelte
<script lang="ts"> import AddDataBackButton from '$lib/widgets/add-data/card-list/AddDataBackButton.svelte'; import type { ComponentItemListOption } from '$lib/components/component-item-list/component-item-list.model'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import type { TimeTravelMapServiceConfiguration } from '$lib/api/managers/configuration'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import { cn } from '$lib/components/shadcn/utils';
type Props = { selectedOption: ComponentItemListOption; onOptionClick: (option: ComponentItemListOption | undefined) => void; i18nRegistry: I18nRegistry; }; let { onOptionClick, i18nRegistry, selectedOption }: Props = $props();
const addDataContext = getAddData(); const mapManager = getMapManager(); const i18n = getI18n(i18nRegistry); const timeTravelConfig = $derived.by(() => selectedOption.timeTravelConfigs ?? []);
let checkboxRefs = $state<Checkbox[]>([]);
function alreadyOnMap(timeTravelConfigId: string): boolean { return !!mapManager.layerList.list.find((x) => x.id === timeTravelConfigId); }
$effect(() => { const selectedIdsForCategory = addDataContext.selectedTimeTravelConfigs.map((f) => f.id); timeTravelConfig.forEach((config, index) => { const checkbox = checkboxRefs[index]; if (checkbox) { checkbox.checked = selectedIdsForCategory.indexOf(config.id) > -1; } }); });
function toggleIndex(timeTravelConfig: TimeTravelMapServiceConfiguration, index: number) { const checkbox = checkboxRefs[index]; const active = checkbox.checked; checkbox.checked = !active; addDataContext.toggleTimeTravelConfig(timeTravelConfig, !active); }</script>
<AddDataBackButton class="gv-ml-[-6px] gv-mt-6" {i18nRegistry} onclick={() => onOptionClick(undefined)} /><div class="gv-pb-3 gv-pt-2 gv-font-bold gv-text-2xl">{i18n.translate(selectedOption.label)}</div><div class="gv-h-full"> {#each timeTravelConfig as timeTravelConfig, index} {@const isAlreadyOnMap = alreadyOnMap(timeTravelConfig.id)} <div title={isAlreadyOnMap ? i18n('common.already-on-map') : timeTravelConfig.label} class={cn( 'gv-flex gv-w-full gv-items-center gv-p-2 gv-border-b-[0.5px] gv-border-grey-300', isAlreadyOnMap ? '' : 'hover:gv-bg-primary/10', )} > <div class="gv-w-[5%] gv-mt-[1px] gv-flex gv-items-center"> <Checkbox disabled={isAlreadyOnMap} bind:this={checkboxRefs[index]} onCheckedChange={() => toggleIndex(timeTravelConfig, index)} /> </div> <div class="gv-w-[95%] gv-flex gv-items-center"> <button class={cn('gv-flex gv-w-full', isAlreadyOnMap ? 'gv-opacity-50' : '')} onclick={() => !isAlreadyOnMap && toggleIndex(timeTravelConfig, index)} > <div class="gv-w-full gv-text-left"> {timeTravelConfig.label} </div> </button> </div> </div> {/each}</div>packages/common/src/lib/widgets/add-data/add-data-from-categories/RootCategoryList.svelte
<script lang="ts"> import { AddDataCurrentScreen, getAddData } from '$lib/widgets/add-data/add-data.svelte'; import type { AddDataConfig } from '$lib/widgets/add-data/add-data.config'; import type { ComponentItemListOption } from '$lib/components/component-item-list/component-item-list.model.js'; import AddDataCategoryTree from '$lib/widgets/add-data/add-data-from-categories/AddDataCategoryTree.svelte'; import AddDataBackButton from '$lib/widgets/add-data/card-list/AddDataBackButton.svelte'; import AddDataCswRecordList from '$lib/widgets/add-data/add-data-from-categories/AddDataCswRecordList.svelte'; import ComponentItemList from '$lib/components/component-item-list/ComponentItemList.svelte'; import AddDataBottomButtons from '$lib/widgets/add-data/card-list/AddDataBottomButtons.svelte'; import AddDataTimeTravelSeries from '$lib/widgets/add-data/add-data-from-categories/AddDataTimeTravelSeries.svelte'; import { getI18n } from '$lib/api/managers/i18n';
type Props = { fullConfig: AddDataConfig; }; let { fullConfig }: Props = $props();
const { config, i18n } = fullConfig; const addDataContext = getAddData(); const _i18n = getI18n(i18n);
let selectedOption = $state<ComponentItemListOption | undefined>();
function backFromSearch() { selectedOption = undefined; addDataContext.currentSearchText = ''; addDataContext.navigate(AddDataCurrentScreen.MAIN); }
function onCategoryOptionClicked(option: ComponentItemListOption | undefined): void { selectedOption = option; if (option) { addDataContext.navigate(AddDataCurrentScreen.CATEGORIES); } else { addDataContext.navigate(AddDataCurrentScreen.MAIN); } }
function onTimeTravelSelected(option: ComponentItemListOption): void { selectedOption = option; addDataContext.navigate(AddDataCurrentScreen.TIME_TRAVEL); }</script>
<div class="gv-max-h-[55vh] gv-overflow-y-auto gv-flex gv-flex-col"> {#if addDataContext.currentScreen === AddDataCurrentScreen.MAIN} <div> <ComponentItemList options={config.categories} onItemClick={(option) => onCategoryOptionClicked(option)} i18nData={fullConfig.i18n} /> <ComponentItemList options={config.timeTravelSeries} onItemClick={(option) => onTimeTravelSelected(option)} i18nData={fullConfig.i18n} /> </div> {:else if addDataContext.currentScreen === AddDataCurrentScreen.CATEGORIES && selectedOption} <AddDataCategoryTree parentOption={selectedOption} i18nRegistry={fullConfig.i18n} onOptionClick={(opt) => onCategoryOptionClicked(opt)} /> {:else if addDataContext.currentScreen === AddDataCurrentScreen.TIME_TRAVEL && selectedOption} <AddDataTimeTravelSeries {selectedOption} i18nRegistry={fullConfig.i18n} onOptionClick={(opt) => onCategoryOptionClicked(opt)} /> {:else if addDataContext.currentScreen === AddDataCurrentScreen.SEARCH_RESULT} <AddDataBackButton i18nRegistry={fullConfig.i18n} onclick={backFromSearch} /> <AddDataCswRecordList /> {/if}</div><div class="gv-pt-5"> <AddDataBottomButtons selectionCount={addDataContext.getSelectedCswRecordsCount()} emptyButtonLabel={_i18n('EMPTY')} addButtonLabel={_i18n('ADD')} onAddClicked={() => addDataContext.addSelectedData()} onEmptyClicked={() => addDataContext.emptySelection()} /></div>packages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { apiFeatureSymbolsSchema } from '$lib/api/symbol';import { type infer as zInfer, z } from 'zod';import { SpatialFileType } from './add-data-from-file.model';import { Projections } from '$lib/api/managers/projection';import { addDataFromFileI18n } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n';
export const addDataFromFileConfigSchema = z .object({ fileTypes: z .enum(SpatialFileType) .array() .default([ SpatialFileType.GeoJSON, SpatialFileType.SHP, SpatialFileType.CSV, SpatialFileType.GPX, SpatialFileType.KML, SpatialFileType.KMZ, SpatialFileType.GML, ]), defaultFileType: z.enum(SpatialFileType).default(SpatialFileType.GeoJSON), defaultShapeFileWkid: z.number().default(Projections.LAMBERT_2008.wkid), defaultSymbolConfig: apiFeatureSymbolsSchema.prefault({}), defaultForceStyle: z.boolean().default(true), resetFormAfterFileAdded: z.boolean().default(false), showResetButton: z.boolean().default(false), closeOnFileAdded: z.boolean().default(true), csvTemplateExamplesUrl: z .string() .optional() .default('https://geoservices.wallonie.be/geoviewer/viewer/poi.zip'), }) .prefault({});export type AddDataFromFileConfig = z.infer<typeof addDataFromFileConfigSchema>;
export const addDataFromFileWidgetConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataFromFileI18n), title: addDataFromFileI18n['add-data-from-file-title'], inToolbar: inToolbarSchemaFrom({ type: 'button', icon: { lucide: 'Plus', }, label: addDataFromFileI18n['add-data-from-file-title'], }), config: addDataFromFileConfigSchema,});
export type AddDataConfigFromFileWidget = zInfer<typeof addDataFromFileWidgetConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { addDataFromFileWidgetConfigSchema, type AddDataConfigFromFileWidget } from './add-data-from-file.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddDataFromFileWidget.svelte').then((AddDataFromFileWidget) => widgetFactorySvelte(AddDataFromFileWidget), ), schema: () => addDataFromFileWidgetConfigSchema,} satisfies WidgetDeclaration;
export type AddDataFromFileProps = WidgetProps<AddDataConfigFromFileWidget>;packages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addDataFromFileI18n = { 'add-data-from-file-title': { fr: 'Ajouter des données', nl: 'NL - Ajouter des données', }, 'add-data-from-file-sub-title': { fr: 'Depuis un fichier', nl: 'NL - Depuis un fichier', }, 'add-data': { fr: 'Ajouter les données', nl: 'NL - Ajouter les données', }, 'csv-tips-button': { fr: "Quelques points d'attention à respecter avec un fichier CSV. Plus d'infos", nl: "NL - Quelques points d'attention à respecter avec un fichier CSV. Plus d'infos", }, 'csv-tips-text': { fr: `Pour fonctionner, cet outil a besoin d'un fichier CSV. Attention, l'ajout de fichiers .csv d'une taille supérieure à 0.5 Mb peut dégrader les performances de votre navigateur. Le fichier CSV doit respecter le format suivant: <ul class="gv-list-disc gv-ml-6"> <li>La première ligne est constituée des titres de champs (noms des colonnes).</li> <li>En fonction du type d’information que vous souhaitez ajouter, renseignez les colonnes suivantes :</li> <ul class="gv-list-disc gv-ml-6"> <li>soit un champ nommé "X" et un champ nommé "Y" reprenant les coordonnées du point exprimées en Lambert Belge 1972 (exemple : 183635)</li> <li>soit un champ nommé "latitude" et un champ nommé "longitude" reprenant les coordonnées en degrés décimales exprimées en wgs84 (exemple : 4.84250)</li> <li>soit un champ nommé "capakey" reprenant la clé cadastrale (exemple : 92074A0010/00B000 – ce champ résulte de la concaténation des valeurs : CODE_DIVISION + SECTION + RADICAL + / + BIS + EXPOSANT + PUISSANCE)</li> <li>soit des champs nommés "Commune","Rue","Numéro" et éventuellement "Code Postal" reprenant une adresse (exemple : "Namur" "St Nicolas" "127")</li> </ul> </ul> `, nl: "NL - Quelques points d'attention à respecter avec un fichier CSV. Plus d'infos", }, 'data-type': { fr: 'Type de données', nl: 'NL - Type de données', }, 'download-csv-examples': { fr: 'Télécharger les examples CSV', nl: 'NL - Télécharger les examples CSV', }, 'file-url': { fr: 'Url du fichier', nl: 'NL - Url du fichier', }, 'file-upload': { fr: 'Upload du fichier', nl: 'NL - Upload du fichier', }, 'service-name': { fr: 'Nom de la couche', nl: 'Naam van de layer', }, 'loaded-layers': { fr: "Couches chargées depuis l'URL", nl: "NL - Couches chargées depuis l'URL", }, 'shape-file-wkid-message': { fr: "Si le fichier .prj n'est pas présent, les données seront considérées comment étant en EPSG:{{wkid}}", nl: "NL - Si le fichier .prj n'est pas présent, les données seront considérées comment étant en EPSG:{{wkid}}", }, 'style-config': { fr: 'Configuration du style', nl: 'NL - Configuration du style', }, 'force-style': { fr: 'Forcer le style', nl: 'NL Forcer le style', }, 'DASHSTYLE-DASH': { fr: 'Pointillés', nl: 'NL - Pointillés', }, 'DASHSTYLE-DASHDOT': { fr: 'Pointillés - points', nl: 'NL - Pointillés - points', }, 'DASHSTYLE-DOT': { fr: 'Points', nl: 'NL - Points', },} satisfies I18nRegistry;
export type AddDataFromFileI18n = I18nRegistry<keyof typeof addDataFromFileI18n>;packages/common/src/lib/widgets/add-data/add-data-from-file/add-data-from-file.model.ts
import type { ApiFeature } from '$lib/api/feature';
export enum SpatialFileType { GeoJSON = 'GeoJSON', SHP = 'SHP', CSV = 'CSV', KML = 'KML', XLSX = 'XLSX', KMZ = 'KMZ', GPX = 'GPX', GML = 'GML',}
export interface ParsingResult { features: ApiFeature[]; label: string; errorMessages?: string[];}
export type SpatialFileTypeConfig = { spatialFileType: SpatialFileType; allowedExtensions: string[]; fileExtension: string; contentType: string; exportAvailable: boolean;};
export const APPLICATION_JSON = 'application/json';export const APPLICATION_GPX = 'application/gpx+xml';export const APPLICATION_CSV = 'application/csv';export const APPLICATION_XML = 'application/xml';export const APPLICATION_ZIP = 'application/zip';export const APPLICATION_XLXS = 'application/xlxs';export const APPLICATION_GML = 'application/gml';
export const SpatialFileInfo: Record<SpatialFileType, SpatialFileTypeConfig> = { [SpatialFileType.GeoJSON]: { spatialFileType: SpatialFileType.GeoJSON, allowedExtensions: ['.json'], fileExtension: '.json', contentType: APPLICATION_JSON, exportAvailable: true, }, [SpatialFileType.SHP]: { spatialFileType: SpatialFileType.SHP, allowedExtensions: ['.zip'], fileExtension: '.zip', contentType: APPLICATION_ZIP, exportAvailable: true, }, [SpatialFileType.CSV]: { spatialFileType: SpatialFileType.CSV, allowedExtensions: ['.csv'], fileExtension: '.csv', contentType: APPLICATION_CSV, exportAvailable: true, }, [SpatialFileType.GPX]: { spatialFileType: SpatialFileType.GPX, allowedExtensions: ['.gpx'], fileExtension: '.gpx', contentType: APPLICATION_GPX, exportAvailable: true, }, [SpatialFileType.KML]: { spatialFileType: SpatialFileType.KML, allowedExtensions: ['.kml'], fileExtension: '.kml', contentType: APPLICATION_XML, exportAvailable: true, }, [SpatialFileType.KMZ]: { spatialFileType: SpatialFileType.KMZ, allowedExtensions: ['.kmz'], fileExtension: '.kmz', contentType: APPLICATION_ZIP, exportAvailable: false, }, [SpatialFileType.XLSX]: { spatialFileType: SpatialFileType.XLSX, allowedExtensions: ['.xlxs'], fileExtension: '.xlxs', contentType: APPLICATION_XLXS, exportAvailable: true, }, [SpatialFileType.GML]: { spatialFileType: SpatialFileType.GML, allowedExtensions: ['.gml'], fileExtension: '.gml', contentType: APPLICATION_GML, exportAvailable: false, },};packages/common/src/lib/widgets/add-data/add-data-from-file/AddDataFromFile.svelte
<script lang="ts"> import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import { type ApiFeatureSymbols } from '$lib/api/symbol'; import { deepClone, getErrorMessage, nonEmpty } from '$lib/api/utils'; import StringUtils from '$lib/api/utils/string.utils'; import { ApiSelect } from '$lib/components/api-select'; import ApiSymbolEditor from '$lib/components/api-symbol-editor/ApiSymbolEditor.svelte'; import { Button } from '$lib/components/shadcn/ui/button'; import { CollapsibleContent, Root as CollapsibleRoot } from '$lib/components/shadcn/ui/collapsible'; import { Input } from '$lib/components/shadcn/ui/input'; import { Label } from '$lib/components/shadcn/ui/label'; import { type ParsingResult, SpatialFileType } from './add-data-from-file.model'; import Info from 'lucide-svelte/icons/info'; import SpatialFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/SpatialFileInput.svelte'; import { setSpatialInputFileState, SpatialFileInputState, } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import type { AddDataFromFileI18n } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n'; import type { AddDataFromFileConfig } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.config'; import Loader from '$lib/components/common/Loader.svelte'; import { getTopicManager } from '$lib/api/managers/topic'; import { initGraphicMapServiceConfiguration } from '$lib/api/managers/configuration'; import { highlightServiceInToc } from '$lib/widgets/toc/toc.utils'; import { getWidgetManager } from '$lib/api/managers/widget';
interface Props { config: AddDataFromFileConfig; i18nRegistry: AddDataFromFileI18n; }
const { config, i18nRegistry }: Props = $props();
const i18n = getI18n(i18nRegistry ?? {}); const topic = getTopicManager(); const mapManager = getMapManager(); const widgetManager = getWidgetManager(); const geometryEngine = mapManager.tools.geometryEngine; const featureConverter = mapManager.tools.featureConverter; const zoomTool = mapManager.tools.zoom; const spatialInputFileState = setSpatialInputFileState(new SpatialFileInputState(topic)); const addDataContext = getAddData();
let fileInputElement = $state.raw<HTMLInputElement>(); let currentFileType = $state(SpatialFileType.SHP); let parsingResult = $state<ParsingResult[]>([]); let currentSymbolConfig = $state<ApiFeatureSymbols>(config.defaultSymbolConfig); let fileUrl = $state<string>(''); let csvTipShown = $state<boolean>(false);
let uploadType: 'file' | 'url' = 'file'; let loadedUrl = '';
$effect(() => { currentSymbolConfig = deepClone($state.snapshot(config.defaultSymbolConfig)); currentFileType = config.defaultFileType; });
const fileTypesOptions = $derived(config.fileTypes.map((fileType) => ({ label: fileType, value: fileType })));
let fetchingFile = $state(false);
function addMapService(): void { if (!parsingResult) { throw new GeoviewerError(`Unable to parse file`); } const addedMapServices = parsingResult.map((parsingResult) => { const mapServiceConfiguration = initGraphicMapServiceConfiguration({ id: StringUtils.uuid(), label: getNewLabel(parsingResult.label), symbols: $state.snapshot(currentSymbolConfig), }); const graphicMapService = mapManager.addGraphicMapService(mapServiceConfiguration); graphicMapService.addFeatures(parsingResult.features); return graphicMapService; }); addedMapServices.forEach((mapService) => { topic.publish({ type: 'AddData-add-mapService-from-file', layer: mapService, uploadType, fileType: currentFileType, sourceUrl: uploadType === 'url' ? loadedUrl : null, features: featureConverter.geoJSON.toGeoJSON(mapService.features), }); highlightServiceInToc(mapService.id, widgetManager, mapManager); });
const extents = addedMapServices.map((mapService) => mapService.extent).filter(nonEmpty); // TODO - handle projs (instead of assuming we want the 1st element wkid) const fullExtent = geometryEngine.unionExtent(extents); zoomTool.zoomToExtent(fullExtent); if (config.closeOnFileAdded) { addDataContext.closeWidget(); } if (config.resetFormAfterFileAdded) { reset(); } }
function getNewLabel(label: string): string { const labelAlreadyTaken = mapManager.layerList.list.find((x) => x.label === label); if (!labelAlreadyTaken) return label; const numberOfSuffixed = mapManager.layerList.list.filter((x) => x.label.match(/^(.+)_([0-9]+)$/)).length; return `${label}_${numberOfSuffixed + 1}`; }
function reset() { currentFileType = config.defaultFileType; currentSymbolConfig = deepClone(config.defaultSymbolConfig); if (fileInputElement) { fileInputElement.value = ''; } parsingResult = []; }
async function fetchFile() { fetchingFile = true; parsingResult = []; try { const response = await fetch(fileUrl); if (!response.ok) { throw new GeoviewerError(`Error while fetching file ${fileUrl}`); } const fileBlob = await response.blob(); spatialInputFileState.fileFromUrl = new File([fileBlob], 'downloadedFile'); uploadType = 'url'; loadedUrl = $state.snapshot(fileUrl); } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error)); throw new GeoviewerError('Failed to load the url', { cause: error }); } finally { fetchingFile = false; } }
function setParsingResultFromFile(results: ParsingResult[]) { parsingResult = results; uploadType = 'file'; }</script>
<div class="gv-flex gv-flex-col gv-gap-3"> <span class="gv-font-bold gv-text-lg gv-my-2">{i18n('add-data-from-file-sub-title')}</span>
<div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold">{i18n('common.file-format')}</Label> <ApiSelect bind:value={currentFileType} options={fileTypesOptions} dataTestId="AddDataFile-FileTypeSelect" size="sm" /> {#if currentFileType === SpatialFileType.CSV} <button data-test-id="AddDataFile-CSVInfoButton" class="gv-flex gv-items-center gv-text-sm gv-text-start" onclick={() => (csvTipShown = !csvTipShown)} > <Info class="gv-h-4" /> {i18n('csv-tips-button')} </button>
<div class="gv-mt-2"> <CollapsibleRoot bind:open={csvTipShown} class="w-full space-y-2"> <CollapsibleContent class="space-y-2"> <div data-test-id="AddDataFile-CSVInfoText" class="gv-text-xs"> {@html i18n('csv-tips-text')} </div> <a data-test-id="AddDataFile-DownloadCSVTemplates" class="gv-mt-1 gv-text-primary gv-text-sm" href={config.csvTemplateExamplesUrl}>{i18n('download-csv-examples')}</a > </CollapsibleContent> </CollapsibleRoot> </div> {/if} {#if currentFileType === SpatialFileType.SHP} <div class="gv-flex gv-text-sm gv-justify-between gv-w-full"> <div class="gv-flex gv-items-center"> <Info class="gv-h-4 gv-mt-[-2px]" /> {i18n('shape-file-wkid-message', { wkid: config.defaultShapeFileWkid })} </div> </div> {/if} </div>
<SpatialFileInput bind:parsingResult={() => parsingResult, setParsingResultFromFile} {i18nRegistry} {spatialInputFileState} {currentFileType} currentShapeFileWkid={config.defaultShapeFileWkid} />
<div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold" for="add-data-file-input">{i18n('file-url')}</Label> <div class="gv-flex gv-gap-1"> <Input bind:value={fileUrl} data-test-id="AddDataFile-FileUrl" class="gv-flex-1" size="sm" /> <Button class="gv-w-1/5" disabled={!fileUrl || fetchingFile || fileUrl.length === 0} onclick={fetchFile} data-test-id="AddDataFile-LoadFileUrl" size="sm" > {#if fetchingFile} <Loader class="gv-size-4" /> {/if} {i18n('common.load')} </Button> </div> </div>
<div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold">{i18n('style-config')}</Label> <div class="gv-w-1/2 gv-h-10"> <ApiSymbolEditor bind:value={currentSymbolConfig} /> </div> </div>
<div class="gv-flex gv-justify-end gv-gap-2"> {#if config.showResetButton} <Button onclick={() => reset()} data-test-id="AddDataFile-ResetButton" size="sm"> {i18n('common.reset')} </Button> {/if} <Button disabled={spatialInputFileState.loading || !parsingResult || parsingResult.length === 0} onclick={addMapService} data-test-id="AddDataFile-AddButton" size="sm" > {i18n('add-data')} </Button> </div></div>packages/common/src/lib/widgets/add-data/add-data-from-file/AddDataFromFileWidget.svelte
<script lang="ts"> import AddDataFromFile from './AddDataFromFile.svelte'; import type { AddDataFromFileProps } from './add-data-from-file.declaration';
let { fullConfig }: AddDataFromFileProps = $props();</script>
<AddDataFromFile config={fullConfig.config} i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/CSVFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model';
import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter'; import { showToast } from '$lib/components/toast/toast.utils';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.CSV.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; const { features, errorMessages } = await featureConverter.csv.fromCSVFile(file); parsingResult = [ { features: features, label: file.name, errorMessages, }, ]; errorMessages.forEach((error) => { showToast({ level: 'error', message: error }); }); } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.CSV); throw new GeoviewerError('Failed to convert csv', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GeoJSONFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { type SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.GeoJSON.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; const defaultMapServiceName = file.name; parsingResult = [ { features: await featureConverter.geoJSON.fromGeoJSONFile(file), label: defaultMapServiceName, }, ]; } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.GeoJSON); throw new GeoviewerError('Failed to convert geojson', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GMLFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.GML.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { spatialInputFileState.loading = true; const file = FilesUtils.readFileFromEvent(event); await readFileContent(file); }
async function readFileContent(file: File) { try { const features = await featureConverter.gml.fromGMLFile(file); if (features && features.length > 0) { parsingResult = [ { features, label: file.name, }, ]; } } catch (error) { spatialInputFileState.onParseError(SpatialFileType.GeoJSON, SpatialFileType.GML); throw new GeoviewerError(`${error}`); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}></ApiFileInput>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/GPXFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.GPX.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; const features = await featureConverter.gpx.fromGPXFile(file); if (features && features.length > 0) { parsingResult = [ { features, label: file.name, }, ]; } } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.GPX); throw new GeoviewerError('Failed to convert gpx', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/KMLFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter'; import { getFilenameFromFeaturesAttributes } from '$lib/api/tools/converter/api-feature-kml.converter';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.KML.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; const features = await featureConverter.kml.fromKMLFile(file); if (features.length > 0) { parsingResult = [{ features, label: getFilenameFromFeaturesAttributes(features) || file.name }]; } } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.KML); throw new GeoviewerError('Failed to convert kml', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/KMZFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter';
interface Props { parsingResult: ParsingResult[]; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), spatialInputFileState, featureConverter }: Props = $props();
const allowedExtensions = SpatialFileInfo.KMZ.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; const items = await featureConverter.kml.fromKMZFile(file); parsingResult.push(...items); } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.KMZ); throw new GeoviewerError('Failed to convert kmz', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/ShapeFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileInfo, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import { FilesUtils, getErrorMessage } from '$lib/api/utils'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { Projections } from '$lib/api/managers/projection'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { ApiFileInput } from '$lib/components/api-file-input'; import type { ApiFeatureConverter } from '$lib/api/tools/converter/api-feature.converter';
interface Props { parsingResult: ParsingResult[]; currentShapeFileWkid: number; spatialInputFileState: SpatialFileInputState; featureConverter: ApiFeatureConverter; }
let { parsingResult = $bindable([]), currentShapeFileWkid = Projections.LAMBERT_72.wkid, spatialInputFileState, featureConverter, }: Props = $props();
const allowedExtensions = SpatialFileInfo.SHP.allowedExtensions.join(',');
$effect(() => { if (spatialInputFileState.fileFromUrl) { readFileContent(spatialInputFileState.fileFromUrl); } });
async function handleFileChange(event: Event) { const file = FilesUtils.readFileFromEvent(event); if (file) { await readFileContent(file); } }
async function readFileContent(file: File) { try { spatialInputFileState.loading = true; parsingResult = await featureConverter.shapeFile.fromShapeFile(file, currentShapeFileWkid); } catch (error) { spatialInputFileState.onParseError(getErrorMessage(error), SpatialFileType.SHP); throw new GeoviewerError('Failed to convert shapefile', { cause: error }); } finally { spatialInputFileState.loading = false; } }</script>
<ApiFileInput dataTestId="AddDataFile-InputFile" multiple={false} loading={spatialInputFileState.loading} accept={allowedExtensions} onchange={handleFileChange}/>packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte.ts
import { getContext, setContext } from 'svelte';import { TopicManager } from '$lib/api/managers/topic';import type { SpatialFileType } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model';
export class SpatialFileInputState { public loading = $state<boolean>(false); public fileFromUrl = $state<File | undefined>(); private topicManager: TopicManager;
constructor(topicManager: TopicManager) { this.topicManager = topicManager; }
public onParseError(errorMessage: string, fileType?: SpatialFileType): void { this.topicManager.publish({ type: 'AddData-parsing-file-error', fileType, errorMessage, }); }}
const SPATIAL_FILE_INPUT_CONTEXT_KEY = 'SPATIAL_FILE_INPUT_CONTEXT_KEY';
export function setSpatialInputFileState(spatialFileInputState: SpatialFileInputState) { setContext(SPATIAL_FILE_INPUT_CONTEXT_KEY, spatialFileInputState); return getSpatialFileInputState();}
export function getSpatialFileInputState(): SpatialFileInputState { const spatialFileInputState = getContext<SpatialFileInputState>(SPATIAL_FILE_INPUT_CONTEXT_KEY); if (!spatialFileInputState) { throw new Error('SpatialFileInputState not found in context.'); } return spatialFileInputState;}packages/common/src/lib/widgets/add-data/add-data-from-file/spatial-file-input/SpatialFileInput.svelte
<script lang="ts"> import { type ParsingResult, SpatialFileType, } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model'; import GeoJONFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/GeoJSONFileInput.svelte'; import ShapeFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/ShapeFileInput.svelte'; import CSVFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/CSVFileInput.svelte'; import KMLFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/KMLFileInput.svelte'; import KMZFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/KMZFileInput.svelte'; import GPXFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/GPXFileInput.svelte'; import GMLFileInput from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/GMLFileInput.svelte'; import type { SpatialFileInputState } from '$lib/widgets/add-data/add-data-from-file/spatial-file-input/spatial-file-input.state.svelte'; import { Label } from '$lib/components/shadcn/ui/label'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import type { AddDataFromFileI18n } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.i18n';
interface Props { parsingResult: ParsingResult[]; currentFileType: SpatialFileType; currentShapeFileWkid: number; spatialInputFileState: SpatialFileInputState; i18nRegistry: AddDataFromFileI18n; }
let { parsingResult = $bindable([]), currentFileType, currentShapeFileWkid, spatialInputFileState, i18nRegistry, }: Props = $props();
const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const featureConverter = mapManager.tools.featureConverter;</script>
<div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold" for="add-data-file-input">{i18n('file-upload')}</Label> <div class="gv-flex gv-w-full"> {#if currentFileType === SpatialFileType.GeoJSON} <GeoJONFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else if currentFileType === SpatialFileType.SHP} <ShapeFileInput {featureConverter} {spatialInputFileState} bind:parsingResult {currentShapeFileWkid} /> {:else if currentFileType === SpatialFileType.CSV} <CSVFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else if currentFileType === SpatialFileType.KML} <KMLFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else if currentFileType === SpatialFileType.KMZ} <KMZFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else if currentFileType === SpatialFileType.GPX} <GPXFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else if currentFileType === SpatialFileType.GML} <GMLFileInput {featureConverter} {spatialInputFileState} bind:parsingResult /> {:else} <div>Unsupported file type {currentFileType}</div> {/if} </div></div>packages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url-widget.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { z } from 'zod';import { addDataFromUrlI18n } from '$lib/widgets/add-data/add-data-from-url/add-data-from-url.i18n';
export const addDataFromUrlConfigSchema = z .object({ closeOnMapServiceAdded: z.boolean().default(true), showServicePreview: z.boolean().default(false), }) .prefault({});export type AddDataFromUrlConfig = z.infer<typeof addDataFromUrlConfigSchema>;
export const addDataFromUrlWidgetSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataFromUrlI18n), icon: { lucide: 'Plus', }, title: addDataFromUrlI18n['add-data-from-url-title'], inToolbar: inToolbarSchemaFrom({ type: 'button', }), config: addDataFromUrlConfigSchema,});
export type AddDataFromUrlWidgetConfig = z.infer<typeof addDataFromUrlWidgetSchema>;packages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { addDataFromUrlWidgetSchema, type AddDataFromUrlWidgetConfig } from './add-data-from-url-widget.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddDataFromUrlWidget.svelte').then((AddDataFromUrlWidget) => widgetFactorySvelte(AddDataFromUrlWidget), ), schema: () => addDataFromUrlWidgetSchema,} satisfies WidgetDeclaration;
export type AddDataFromUrlProps = WidgetProps<AddDataFromUrlWidgetConfig>;packages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addDataFromUrlI18n = { 'add-data-from-url-title': { fr: 'Ajouter des données', nl: 'NL - Ajouter des données', }, 'add-data-from-url-sub-title': { fr: 'Depuis une URL', nl: 'NL - Depuis une URL', }, 'default-map-service-name': { fr: 'Nouveau map service', nl: 'NL - Nouveau map service', }, 'select-unselect-all': { fr: 'Tout sélectionner / déselectionner', nl: 'NL - Tout sélectionner / déselectionner', }, 'service-type': { fr: 'Type de service', nl: 'NL - Type de service', }, 'data-type': { fr: 'Type de service', nl: 'NL - Type de service', }, 'override-style': { fr: 'Surcharger le style', nl: 'NL - Surcharger le style', }, 'loading-error': { fr: "Impossible de charger le service. l'URL encodée est-elle valide ?", nl: "NL - Impossible de charger le service, l'URL encodée est-elle valide ?", }, 'wms-error': { fr: "L'URL encodée n'est pas conforme, il ne faut pas renseigner de paramètre de requête (?request=GetCapabilities par exemple)", nl: "NL - L'URL encodée n'est pas conforme, il ne faut pas renseigner de paramètre de requête (?request=GetCapabilities par exemple)", }, url: { fr: 'URL du service', nl: 'NL - URL du service', }, 'service-name': { fr: 'Nom de la couche', nl: 'Naam van de layer', }, 'ags-dynamic': { fr: 'ArcGIS Dynamique', nl: 'NL - ArcGIS Dynamique', }, 'ags-tiled': { fr: 'ArcGIS Tuilé', nl: 'NL - ArcGIS Tuilé', }, wms: { fr: 'WMS', nl: 'NL - WMS', },} satisfies I18nRegistry;
export type AddDataFromUrlI18n = I18nRegistry<keyof typeof addDataFromUrlI18n>;packages/common/src/lib/widgets/add-data/add-data-from-url/add-data-from-url.model.ts
import { MapServiceTypes } from '$lib/api/managers/configuration/models/service/map-service-configuration.enum';import type { ArcgisDynamicMapServiceConfiguration, ArcgisFeatureServiceConfiguration, ArcgisTiledMapServiceConfiguration, SublayerConfiguration, SublayerConfigurationMode, WmsMapServiceConfiguration,} from '$lib/api/managers/configuration/models/service/map-service-configuration.types';
export const availableMapServiceTypes = [ { label: 'ArcGIS Dynamic', value: MapServiceTypes.ARCGIS_DYNAMIC, }, { label: 'ArcGIS Tuilé', value: MapServiceTypes.ARCGIS_TILED, }, { label: 'ArcGIS Feature Service', value: MapServiceTypes.ARCGIS_FEATURE_SERVICE, }, { label: 'WMS', value: MapServiceTypes.WMS, },];
export const mapServiceTypeToExampleURL = { ARCGIS_DYNAMIC: 'https://geoservices.wallonie.be/arcgis/rest/services/MOBILITE/RAVEL_VELOROUTES/MapServer', ARCGIS_TILED: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_LAST/MapServer', ARCGIS_FEATURE_SERVICE: 'https://geoservices.test.wallonie.be/arcgis/rest/services/EAU/RES_HYDRO_WAL_SIMPLE/FeatureServer', WMS: 'https://geoservices.wallonie.be/arcgis/services/FORET/FORETANC/MapServer/WMSServer',};
export type AddableWithURLMapServiceTypes = AddFromUrlMapServiceConfiguration['type'];
export type AddFromUrlMapServiceConfiguration = ( | WmsMapServiceConfiguration | ArcgisDynamicMapServiceConfiguration | ArcgisTiledMapServiceConfiguration | ArcgisFeatureServiceConfiguration) & { sublayers?: SublayerConfiguration[]; sublayersConfigMode: SublayerConfigurationMode;};packages/common/src/lib/widgets/add-data/add-data-from-url/AddDataFromUrl.svelte
<script lang="ts"> import type { SublayerParent } from '$lib/api/layers/api-sublayer.svelte'; import { MapServiceTypes } from '$lib/api/managers/configuration/models/service/map-service-configuration.enum'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import type { ApiMapService } from '$lib/api/mapservices'; import StringUtils from '$lib/api/utils/string.utils'; import { ApiSelect } from '$lib/components/api-select'; import { Button } from '$lib/components/shadcn/ui/button'; import { Input } from '$lib/components/shadcn/ui/input'; import { Label } from '$lib/components/shadcn/ui/label'; import { onDestroy, tick } from 'svelte'; import type { AddDataFromUrlConfig } from './add-data-from-url-widget.config'; import { type AddFromUrlMapServiceConfiguration, availableMapServiceTypes, mapServiceTypeToExampleURL, } from './add-data-from-url.model'; import LayerHierarchySelect from './LayerHierarchySelect.svelte'; import type { AddDataFromUrlI18n } from '$lib/widgets/add-data/add-data-from-url/add-data-from-url.i18n'; import { getTopicManager } from '$lib/api/managers/topic'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import { getWidgetManager } from '$lib/api/managers/widget'; import { highlightServiceInToc } from '$lib/widgets/toc/toc.utils'; import { showToast } from '$lib/components/toast/toast.utils'; import { isCapabilitiesLoadingError, isFeatureService } from '$lib/api/utils'; import ApiSymbolEditor from '$lib/components/api-symbol-editor/ApiSymbolEditor.svelte'; import { type ApiFeatureSymbols, apiFeatureSymbolsSchema } from '$lib/api/symbol'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { createTree, extractSelected } from '$lib/widgets/add-data/add-data-from-url/tree-item.svelte';
interface Props { config: AddDataFromUrlConfig; i18nRegistry: AddDataFromUrlI18n; }
const { i18nRegistry, config }: Props = $props(); const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const widgetManager = getWidgetManager(); const addDataContext = getAddData(); const topic = getTopicManager();
let currentServiceType = $state< | MapServiceTypes.WMS | MapServiceTypes.ARCGIS_DYNAMIC | MapServiceTypes.ARCGIS_TILED | MapServiceTypes.ARCGIS_FEATURE_SERVICE >(MapServiceTypes.ARCGIS_DYNAMIC); let currentMapServiceUrl = $state(''); let currentMapService = $state<ApiMapService | undefined>(undefined); const tree = $derived(currentMapService ? createTree(currentMapService, null) : null);
let currentSymbolConfig = $state<ApiFeatureSymbols | undefined>(undefined); let overrideFeatureStyle = $state<boolean>(false);
$effect(() => { if (overrideFeatureStyle && !currentSymbolConfig) { currentSymbolConfig = apiFeatureSymbolsSchema.parse({}); }
if (!overrideFeatureStyle && currentSymbolConfig) { currentSymbolConfig = undefined; if (currentMapService && isFeatureService(currentMapService)) { currentMapService.resetDefaultRenderer(); } } });
$effect(() => { if (currentSymbolConfig && currentMapService && isFeatureService(currentMapService)) { currentMapService.updateSymbols(currentSymbolConfig); } });
function getMapServiceConfig(): AddFromUrlMapServiceConfiguration { return { id: StringUtils.uuid(), type: currentServiceType, label: '', url: currentMapServiceUrl, visible: true, identifiable: true, opacity: config.showServicePreview ? 1 : 0, sublayers: [], sublayersConfigMode: 'modify', removable: true, toc: { visible: config.showServicePreview, open: false, }, symbols: currentSymbolConfig, }; }
function loadMapService() { removeMapServicePreview(); const mapServiceConfig = getMapServiceConfig(); currentMapService = mapManager.addMapService(mapServiceConfig); if (currentMapService) { currentMapService.onLoad( () => { if (currentMapService && !currentMapService.label) { currentMapService.label = i18n('default-map-service-name'); } }, (error) => onLoadError(error), ); } }
function onLoadError(error: unknown) { removeMapServicePreview(); const message = currentServiceType === MapServiceTypes.WMS && isCapabilitiesLoadingError(error) ? i18n('wms-error') : i18n('loading-error'); showToast({ level: 'error', message }); }
function addMapService() { if (!currentMapService || !tree) { return; } const layer = extractSelected(tree); layer.toc.visible = true; layer.opacity = 1; topic.publish({ type: 'AddData-add-mapService-from-url', layer: layer, }); highlightServiceInToc(layer.id, widgetManager, mapManager); currentMapService = undefined; if (config.closeOnMapServiceAdded) { tick().then(() => { addDataContext.closeWidget(); }); } }
function reset() { removeMapServicePreview(); }
function removeMapServicePreview() { if (currentMapService) { mapManager.layerList.remove(currentMapService); currentMapService = undefined; } }
onDestroy(() => removeMapServicePreview());</script>
<div class="gv-flex gv-flex-col gv-gap-3"> <span class="gv-font-bold gv-text-lg gv-my-2">{i18n('add-data-from-url-sub-title')}</span>
<div class="gv-flex gv-flex-col gv-gap-3"> <div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold">{i18n('service-type')}</Label> <ApiSelect dataTestId="AddDataUrl-ServiceTypeSelect" bind:value={currentServiceType} options={availableMapServiceTypes} size="sm" /> </div>
<div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold" for="add-data-url-input">{i18n('url')}</Label> <div class="gv-flex gv-gap-1"> <Input id="add-data-url-input" bind:value={currentMapServiceUrl} class="gv-flex-1" data-test-id="AddDataUrl-ServiceUrl" size="sm" /> <Button disabled={!currentMapServiceUrl || currentMapServiceUrl.length === 0} onclick={() => loadMapService()} class="gv-w-1/5" variant="secondary" data-test-id="AddDataUrl-LoadButton" size="sm" > {i18n('common.load')} </Button> </div> <div class="gv-flex"> <span class="gv-text-sm gv-text-grey-600"> {i18n('common.example')}: {mapServiceTypeToExampleURL[currentServiceType]} </span> </div> </div>
{#if currentMapService && tree} <div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold" for="add-data-service-name-input">{i18n('service-name')}</Label> <Input id="add-data-service-name-input" bind:value={currentMapService.label} data-test-id="AddDataUrl-ServiceName" /> </div> <div class="gv-flex gv-flex-col gv-gap-1"> <Label class="gv-font-bold" for="add-data-service-name-input">{i18n('loaded-layers')}</Label> <div class="gv-max-h-[25vh] gv-overflow-y-auto gv-flex gv-flex-col gv-gap-4"> <LayerHierarchySelect item={tree} label={i18n('select-unselect-all')} checkboxDataTestId="AddDataUrl-SelectAll" /> {#if currentServiceType === 'ARCGIS_FEATURE_SERVICE'} <div class="gv-flex gv-flex-col gv-gap-2"> <div class="gv-flex gv-gap-1"> <Checkbox title={i18n('override-style')} bind:checked={overrideFeatureStyle} /> <Label class="gv-font-bold">{i18n('override-style')}</Label> </div> {#if overrideFeatureStyle && currentSymbolConfig} <div class="gv-w-1/2 gv-h-10"> <ApiSymbolEditor bind:value={currentSymbolConfig} /> </div> {/if} </div> {/if} </div> </div> {/if} </div>
<div class="gv-flex gv-justify-end gv-gap-2"> <Button disabled={!currentMapService} onclick={addMapService} data-test-id="AddDataUrl-AddButton" size="sm"> {i18n('common.add')} </Button> <Button onclick={() => reset()} variant="secondary" data-test-id="AddDataUrl-Reset" size="sm"> {i18n('common.reset')} </Button> </div></div>packages/common/src/lib/widgets/add-data/add-data-from-url/AddDataFromUrlWidget.svelte
<script lang="ts"> import AddDataFromUrl from './AddDataFromUrl.svelte'; import type { AddDataFromUrlProps } from './add-data-from-url.declaration';
let { fullConfig }: AddDataFromUrlProps = $props();</script>
<AddDataFromUrl config={fullConfig.config} i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-from-url/LayerHierarchySelect.svelte
<script lang="ts"> import SvelteSelf from './LayerHierarchySelect.svelte'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { Label } from '$lib/components/shadcn/ui/label'; import type { TreeItem } from './tree-item.svelte.js';
interface Props { item: TreeItem; label?: string | undefined; checkboxDataTestId?: string; class?: string; }
let { item, label = undefined, checkboxDataTestId, class: className }: Props = $props(); const testId = $derived(checkboxDataTestId ?? `LayerHierarchySelect-${item.service.id}-Checkbox`);</script>
<div class={className}> <div class="gv-flex gv-items-center gv-space-x-2" data-test-id={`LayerHierarchySelect-${item.service.id}`}> <Checkbox bind:checked={item.selected} indeterminate={!item.allSublayerAreSelected && item.selected} data-test-id={testId} id={testId} /> <Label for={testId}>{label ?? item.service.label}</Label> </div> {#if item.children.length > 0} <div class="gv-flex gv-flex-col gv-gap-2 gv-mt-2 gv-ml-4"> {#each item.children.toReversed() as child (child.service.id)} <SvelteSelf item={child} /> {/each} </div> {/if}</div>packages/common/src/lib/widgets/add-data/add-data-from-url/tree-item.svelte.ts
import type { ApiMapService } from '$lib/api/mapservices';import { type ApiSublayer, hasSublayers } from '$lib/api/layers';import { isMapService } from '$lib/api/utils';
export class TreeItem { _selected = $state(true); public children: TreeItem[] = $state([]); public readonly allSublayerAreSelected = $derived.by(() => { if (this.children.length === 0) return true; return this.children.every((c) => c.selected); });
constructor( public readonly service: ApiMapService | ApiSublayer, public readonly parent: TreeItem | null, ) {}
public get selected() { return this._selected; }
public set selected(checked: boolean) { this._selected = checked; setChildrenSelected(this, checked); if (checked) { setParentSelected(this, checked); } }}
function setParentSelected(item: TreeItem, selected: boolean) { item._selected = selected; if (item.parent) { setParentSelected(item.parent, selected); }}function setChildrenSelected(item: TreeItem, selected: boolean) { if (item.children) { for (const child of item.children) { child._selected = selected; setChildrenSelected(child, selected); } }}
export function createTree(service: ApiMapService | ApiSublayer, parent: TreeItem | null): TreeItem { const item = new TreeItem(service, parent); if (hasSublayers(service) && service.sublayers?.length) { item.children = service.sublayers?.list.map((c) => createTree(c, item)); } return item;}
export function extractSelected(item: TreeItem): ApiMapService { if (!isMapService(item.service)) { throw new Error('Item service is not a mapService'); }
function removeNotSelected(item: TreeItem) { const service = item.service; if (hasSublayers(service)) { item.children.forEach((child) => { if (child.selected) { removeNotSelected(child); } else { service.sublayers.remove(child.service as ApiSublayer); } }); } }
removeNotSelected(item);
return item.service;}packages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import { z } from 'zod';import { addDataPopularsI18n } from '$lib/widgets/add-data/add-data-populars/add-data-populars.i18n';import { mapServiceConfigurationSchema } from '$lib/api/managers/configuration';
const geoportailServiceConfigSchema = z.object({ id: z.string().optional(), type: z.literal('METADATA'), metadataId: z.string().trim(), label: z.string().optional(),});
export const addDataPopularsConfigSchema = z .object({ popularServices: z.array(z.union([mapServiceConfigurationSchema, geoportailServiceConfigSchema])).default([]), trendingServices: z.array(z.union([mapServiceConfigurationSchema, geoportailServiceConfigSchema])).default([]), }) .prefault({});export type AddDataPopularsConfig = z.infer<typeof addDataPopularsConfigSchema>;export type PopularConfigItem = AddDataPopularsConfig['popularServices'][number];
export const addDataPopularsWidgetConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataPopularsI18n), title: addDataPopularsI18n['add-data-populars-title'], inToolbar: inToolbarSchemaFrom(false), config: addDataPopularsConfigSchema,});
export type AddDataPopularsWidgetConfig = z.infer<typeof addDataPopularsWidgetConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { type AddDataPopularsWidgetConfig, addDataPopularsWidgetConfigSchema } from './add-data-populars.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddDataPopularsWidget.svelte').then((AddDataPopularsWidget) => widgetFactorySvelte(AddDataPopularsWidget), ), schema: () => addDataPopularsWidgetConfigSchema,} satisfies WidgetDeclaration;
export type AddDataPopularsProps = WidgetProps<AddDataPopularsWidgetConfig>;packages/common/src/lib/widgets/add-data/add-data-populars/add-data-populars.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addDataPopularsI18n = { 'add-data-populars-title': { fr: 'Données populaires', nl: 'NL - Données populaires', }, 'trending-data': { fr: "Données d'actualité", nl: "NL - Données d'actualité", },} satisfies I18nRegistry;
export type AddDataPopularsI18n = I18nRegistry<keyof typeof addDataPopularsI18n>;packages/common/src/lib/widgets/add-data/add-data-populars/AddDataPopulars.svelte
<script lang="ts"> import { type MapServiceConfiguration } from '$lib/api/managers/configuration'; import type { AddDataPopularsI18n } from '$lib/widgets/add-data/add-data-populars/add-data-populars.i18n'; import type { AddDataPopularsConfig } from '$lib/widgets/add-data/add-data-populars/add-data-populars.config'; import { getMapManager } from '$lib/api/map'; import CardList from '$lib/widgets/add-data/card-list/CardList.svelte'; import { cn } from '$lib/components/shadcn/utils'; import { getI18n } from '$lib/api/managers/i18n'; import { highlightServiceInToc } from '$lib/widgets/toc/toc.utils'; import { getWidgetManager } from '$lib/api/managers/widget'; import { getTopicManager } from '$lib/api/managers/topic';
interface Props { config: AddDataPopularsConfig; i18nRegistry: AddDataPopularsI18n; class?: string; }
let { config, i18nRegistry, class: className }: Props = $props();
const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const widgetManager = getWidgetManager(); const topic = getTopicManager();
function onItemClick(service: MapServiceConfiguration, source: 'populars' | 'trending') { if (!mapManager.layerList.findById(service.id)) { const layer = mapManager.addMapService(service); topic.publish({ type: 'AddData-add-mapService-from-populars', layer, source, }); } highlightServiceInToc(service.id, widgetManager, mapManager); }</script>
<div class={cn('gv-max-h-[55vh] gv-overflow-y-auto gv-flex gv-flex-col gv-gap-5 gv-pb-0.5', className)}> <CardList items={config.popularServices} onItemClick={(service) => onItemClick(service, 'populars')} {i18nRegistry} dataTestIdPrefix="popular-" />
{#if config.trendingServices.length} <div class="gv-flex gv-flex-col gv-gap-1"> <span class="gv-font-bold gv-text-lg">{i18n('trending-data')}</span> <CardList items={config.trendingServices} onItemClick={(service) => onItemClick(service, 'trending')} {i18nRegistry} dataTestIdPrefix="trending-" /> </div> {/if}</div>packages/common/src/lib/widgets/add-data/add-data-populars/AddDataPopularsWidget.svelte
<script lang="ts"> import AddDataPopulars from './AddDataPopulars.svelte'; import type { AddDataPopularsProps } from './add-data-populars.declaration';
let { fullConfig }: AddDataPopularsProps = $props();</script>
<AddDataPopulars config={fullConfig.config} i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n/i18n.schema';import type { z } from 'zod';import { addDataPresetsI18n } from '$lib/widgets/add-data/add-data-presets/add-data-presets.i18n';
export const addDataPresetsWidgetConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(addDataPresetsI18n), title: addDataPresetsI18n['add-data-presets-title'], inToolbar: inToolbarSchemaFrom(false),});
export type AddDataPresetsWidgetConfig = z.infer<typeof addDataPresetsWidgetConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { type AddDataPresetsWidgetConfig, addDataPresetsWidgetConfigSchema } from './add-data-presets.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./AddDataPresetsWidget.svelte').then((AddDataPresetsWidget) => widgetFactorySvelte(AddDataPresetsWidget), ), schema: () => addDataPresetsWidgetConfigSchema,} satisfies WidgetDeclaration;
export type AddDataPresetsProps = WidgetProps<AddDataPresetsWidgetConfig>;packages/common/src/lib/widgets/add-data/add-data-presets/add-data-presets.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addDataPresetsI18n = { 'add-data-presets-title': { fr: 'Vues prédéfinies', nl: 'NL - Vues prédéfinies', }, 'confirm-message': { fr: "L'ajout de la vue prédéfinie va supprimer toutes les données présentes actuellement sur la carte. Voulez-vous poursuivre l'opération ?", nl: "NL - L'ajout de la vue prédéfinie va supprimer toutes les données présentes actuellement sur la carte. Voulez-vous poursuivre l'opération ?", }, 'confirm-label': { fr: 'Oui, ajouter la vue prédéfinie', nl: 'NL - Oui, ajouter la vue prédéfinie', }, 'confirm-title': { fr: 'Attention', nl: 'NL - Attention', }, description: { fr: "Une vue prédéfinie permet d'ajouter un ensemble de données relatif à un thème choisi", nl: "NL - Une vue prédéfinie permet d'ajouter un ensemble de donénes relatif à un thème choisi", }, 'footer-warning': { fr: 'Attention, les données ajoutées auparavant seront supprimées de votre sélection afin de faire place à ce nouvel ensemble.', nl: 'NL - Attention, les données ajoutées auparavant seront supprimées de votre sélection afin de faire place à ce nouvel ensemble.', },} satisfies I18nRegistry;
export type AddDataPresetsI18n = I18nRegistry<keyof typeof addDataPresetsI18n>;packages/common/src/lib/widgets/add-data/add-data-presets/AddDataPresets.svelte
<script lang="ts"> import { getContextManager } from '$lib/api/managers/context'; import { type ContextConfiguration, getConfigurationManager } from '$lib/api/managers/configuration'; import type { AddDataPresetsI18n } from '$lib/widgets/add-data/add-data-presets/add-data-presets.i18n'; import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte'; import { getI18n } from '$lib/api/managers/i18n'; import { cn } from '$lib/components/shadcn/utils'; import IconComp from '$lib/components/icon/Icon.svelte'; import { Button } from '$lib/components/shadcn/ui/button'; import Info from 'lucide-svelte/icons/info'; import { getMapManager } from '$lib/api/map'; import { getTopicManager } from '$lib/api/managers/topic'; import { tick } from 'svelte';
interface Props { i18nRegistry: AddDataPresetsI18n; }
let { i18nRegistry }: Props = $props();
const configurationManager = getConfigurationManager(); const contextManager = getContextManager(); const i18n = getI18n(i18nRegistry); const mapManager = getMapManager(); const topic = getTopicManager();
const contexts = configurationManager.contextsConfiguration ?? [];
let confirmOpened = $state<boolean>(false); let selectedContext: ContextConfiguration | undefined;
function onContextClick(context: ContextConfiguration) { confirmOpened = true; selectedContext = context; }
async function onContextConfirm() { if (selectedContext) { const context = selectedContext; const previousLayerIds = new Set(mapManager.layerList.list.map((layer) => layer.id)); await contextManager.onContextChange(context); await tick(); mapManager.layerList.list .filter((layer) => !previousLayerIds.has(layer.id)) .forEach((layer) => { topic.publish({ type: 'AddData-add-mapService-from-predefined-view', layer, context: { id: context.id, label: context.label, }, }); }); } }</script>
<div class="gv-mb-3">{i18n('description')}</div>
<div class="gv-flex gv-flex-col gv-gap-y-2"> {#each contexts as context (context.id)} <div class="gv-border gv-border-grey-300 gv-w-full gv-p-2"> <div class="gv-flex gv-flex-row"> <div class="gv-w-1/5"> <IconComp icon={context.icon} alt={`${i18n.translate(context.label)}`} class={cn('gv-h-24 gv-w-full gv-object-cover gv-m-auto')} /> </div> <div class="gv-w-3/5 gv-pl-2 gv-flex gv-flex-col gv-justify-between"> <div> <div class="gv-font-bold">{i18n.translate(context.label)}</div> <div class="gv-text-base gv-pt-1">{i18n.translate(context.description)}</div> </div> <div class="gv-mt-2 gv-self-start"> <Button variant="outline" onclick={() => onContextClick(context)} size="sm"> {i18n('common.select')} </Button> </div> </div> </div> </div> {/each}</div>
<div class="gv-text-secondary gv-text-sm gv-mt-2 gv-flex gv-items-center gv-align-top gv-gap-2"> <Info class="gv-size-6 " /> <p>{i18n('footer-warning')}</p></div>
<ConfirmDialog title={i18n('confirm-title')} message={i18n('confirm-message')} confirmLabel={i18n('confirm-label')} onConfirm={() => onContextConfirm()} bind:open={confirmOpened}/>packages/common/src/lib/widgets/add-data/add-data-presets/AddDataPresetsWidget.svelte
<script lang="ts"> import type { AddDataPresetsProps } from './add-data-presets.declaration'; import AddDataPresets from './AddDataPresets.svelte';
let { fullConfig }: AddDataPresetsProps = $props();</script>
<AddDataPresets i18nRegistry={fullConfig.i18n} />packages/common/src/lib/widgets/add-data/add-data-search/add-data-search.config.ts
import { z } from 'zod';import type { I18nRegistry } from '$lib/api/managers/i18n/i18n.schema';
export const addDataSearchDefaultTrads = { 'clear-input': { fr: 'Vider la recherche', nl: 'NL - Vider la recherche', }, 'search-data': { fr: 'Chercher une donnée', nl: 'NL - Chercher une donnée', }, 'search-results-title': { fr: 'Résultat de la recherche sur le mot "{{searchText}}"', nl: 'NL - Résultat de la recherche sur le mot "{{searchText}}"', }, results: { fr: 'résultat(s)', nl: 'NL - résultat(s)', }, 'order-alphabetic': { fr: 'Alphabétique', nl: 'NL - Alphabétique', }, 'order-pertinence': { fr: 'Pertinence', nl: 'NL - Pertinence', }, 'order-reverse-alphabetic': { fr: 'Alphabétique inversé', nl: 'NL - Alphabétique inversé', }, theme: { fr: 'Thème', nl: 'NL - Thème', },};export type AddDataSearchI18n = I18nRegistry<keyof typeof addDataSearchDefaultTrads>;
export const addDataSearchConfigSchema = z .object({ startQueryDelay: z.number().optional().default(1000), minimumCharactersBeforeSearchStart: z.number().optional().default(3), }) .prefault({});
export type AddDataSearchConfig = z.infer<typeof addDataSearchConfigSchema>;packages/common/src/lib/widgets/add-data/add-data-search/add-data-search.model.ts
export enum SearchSort { ALPHABETIC = 'ALPHABETIC', PERTINENCE = 'PERTINENCE', REVERSE_ALPHABETIC = 'REVERSE_ALPHABETIC',}packages/common/src/lib/widgets/add-data/add-data-search/AddDataSearch.svelte
<script lang="ts"> import { Input } from '$lib/components/shadcn/ui/input'; import Search from 'lucide-svelte/icons/search'; import X from 'lucide-svelte/icons/x'; import { debounceState } from '$lib/api/utils'; import type { AddDataSearchI18n } from '$lib/widgets/add-data/add-data-search/add-data-search.config'; import { getI18n } from '$lib/api/managers/i18n'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte'; import type { AddDataConfig } from '$lib/widgets/add-data/add-data.config';
interface Props { fullConfig: AddDataConfig; i18nRegistry: AddDataSearchI18n; }
let { fullConfig, i18nRegistry }: Props = $props();
const { addDataSearchConfig } = fullConfig.config; const { startQueryDelay, minimumCharactersBeforeSearchStart } = addDataSearchConfig;
const i18n = getI18n(i18nRegistry ?? {}); const addDataContext = getAddData();
let inputElement = $state<HTMLInputElement>();
const debouncedFilter = debounceState(() => addDataContext.currentSearchText, startQueryDelay); const debouncedSearchInput = $derived( debouncedFilter.debounced?.length > 2 && addDataContext.currentResultsSearchText != debouncedFilter.debounced ? debouncedFilter.debounced : null, );
$effect(() => { if (debouncedSearchInput && debouncedSearchInput.length >= minimumCharactersBeforeSearchStart) { addDataContext.searchRecordsByTitle(); } });
function clearInput() { addDataContext.currentSearchText = ''; inputElement?.focus(); }</script>
<div class="gv-relative"> <div class="gv-relative gv-w-full"> <div class="gv-absolute gv-text-primary gv-inset-y-0 gv-left-0 gv-flex gv-items-center gv-pl-3 gv-pointer-events-none" > <Search class="gv-size-4" /> </div>
<Input class="gv-pl-10 gv-outline-none gv-shadow-none gv-border-grey-300 gv-rounded-full gv-text-xl" placeholder={i18n('search-data')} bind:value={addDataContext.currentSearchText} bind:el={inputElement} data-add-data-search-input /> </div> <div class="gv-absolute gv-right-3 gv-top-1/2 gv-transform gv--translate-y-1/2 gv-flex gv-items-center gv-gap-2"> {#if addDataContext.currentSearchText} <button onclick={clearInput} title={i18n('clear-input')} class="gv-cursor-pointer"> <X class="gv-size-4" /> </button> {/if} </div></div>packages/common/src/lib/widgets/add-data/add-data-search/AddDataSearchResults.svelte
<script lang="ts"> import AddDataBackButton from '$lib/widgets/add-data/card-list/AddDataBackButton.svelte'; import type { AddDataSearchI18n } from '$lib/widgets/add-data/add-data-search/add-data-search.config.js'; import { AddDataCurrentScreen, getAddData } from '$lib/widgets/add-data/add-data.svelte'; import { getI18n } from '$lib/api/managers/i18n'; import AddDataCswRecordList from '$lib/widgets/add-data/add-data-from-categories/AddDataCswRecordList.svelte'; import { ApiSelect } from '$lib/components/api-select'; import type { AddDataConfig } from '$lib/widgets/add-data/add-data.config'; import { SearchSort } from '$lib/widgets/add-data/add-data-search/add-data-search.model'; import { ALL_THEMES } from '$lib/widgets/add-data/default-categories';
interface Props { fullConfig: AddDataConfig; i18nRegistry: AddDataSearchI18n; }
let { i18nRegistry, fullConfig }: Props = $props();
const { config } = fullConfig; const addDataContext = getAddData(); const i18n = getI18n(i18nRegistry);
const filterOptions = $derived.by(() => { return [ { label: i18n('common.all'), value: ALL_THEMES, }, ...config.categories.map((cat) => { return { label: i18n.translate(cat.label), value: cat.code, }; }), ]; });
const sortOptionsItems = $derived.by(() => { return [ { label: i18n('order-pertinence'), value: SearchSort.PERTINENCE, }, { label: i18n('order-alphabetic'), value: SearchSort.ALPHABETIC, }, { label: i18n('order-reverse-alphabetic'), value: SearchSort.REVERSE_ALPHABETIC, }, ]; });
function onBack() { addDataContext.currentSearchText = ''; addDataContext.currentSort = SearchSort.ALPHABETIC; addDataContext.currentFilter = ALL_THEMES; addDataContext.navigate(AddDataCurrentScreen.MAIN); }</script>
<AddDataBackButton class="gv-ml-[-6px]" {i18nRegistry} onclick={onBack} /><div class="gv-pb-1 gv-pt-2 gv-font-bold gv-text-2xl"> {i18n('search-results-title', { searchText: addDataContext.currentResultsSearchText })}</div>
{#if !addDataContext.loading} <div class="gv-font-bold gv-text-l gv-pb-2"> {addDataContext.currentSearchResults.length} {i18n('results')} </div>{/if}
<div class="gv-mt-2 gv-mb-3 gv-w-full gv-flex gv-justify-between"> <div class="gv-font-bold gv-flex gv-flex-row gv-items-center"> <div class="gv-mr-2">{i18n('common.filter')}</div> <div class="gv-w-48"> <ApiSelect placeholder={i18n('theme')} bind:value={addDataContext.currentFilter} options={filterOptions} /> </div> </div> <div class="gv-font-bold gv-flex gv-flex-row gv-items-center"> <div class="gv-mr-2">{i18n('common.order')}</div> <div class="gv-w-48"> <ApiSelect bind:value={addDataContext.currentSort} options={sortOptionsItems} /> </div> </div></div>
<AddDataCswRecordList />packages/common/src/lib/widgets/add-data/add-data.i18n.ts
import { type I18nRegistry } from '$lib/api/managers/i18n';import { addDataCategoriesTranslations } from '$lib/widgets/add-data/add-data-categories.translations';import { addDataSearchDefaultTrads } from '$lib/widgets/add-data/add-data-search/add-data-search.config';import { addDataAdvancedOptionsI18n } from '$lib/widgets/add-data/add-data-advanced-options/add-data-advanced-options.i18n';import { addDataPresetsI18n } from '$lib/widgets/add-data/add-data-presets/add-data-presets.i18n';import { addDataPopularsI18n } from '$lib/widgets/add-data/add-data-populars/add-data-populars.i18n';
export const addDataI18n = { 'add-data-title': { fr: 'Ajouter des données', nl: 'NL - Ajouter des données', }, 'tab-populars': { fr: 'Accès rapide', nl: 'NL - Accès rapide', }, 'tab-categories': { fr: 'Catégories', nl: 'NL - Catégories', }, 'tab-predefined-views': { fr: 'Vues prédéfinies', nl: 'NL - Vues prédéfinies', }, 'tab-advanced-options': { fr: 'Options avancées', nl: 'NL - Options avancées', }, 'tab-catalog': { fr: 'Catalogue', nl: 'NL - Catalogue', }, CATEGORIES: { fr: 'Catégories', nl: 'NL - Catégories', }, ADD: { fr: 'Ajouter les données sélectionnées', nl: 'NL - Ajouter les données sélectionnées', }, EMPTY: { fr: 'Vider', nl: 'NL - Vider', }, ...addDataPopularsI18n, ...addDataPresetsI18n, ...addDataAdvancedOptionsI18n, ...addDataSearchDefaultTrads, ...addDataCategoriesTranslations,} satisfies I18nRegistry;packages/common/src/lib/widgets/add-data/add-data.svelte.ts
import { getContext, setContext } from 'svelte';import type { AddDataCategory, AddDataConfig, AddDataMetawalApiParams } from '$lib/widgets/add-data/add-data.config';import { type ApiCswRecord } from '$lib/api/clients';import type WidgetManager from '$lib/api/managers/widget/widget.manager.svelte';import { SearchSort } from '$lib/widgets/add-data/add-data-search/add-data-search.model';import { getGeoportailUrlFromUuid, sort } from '$lib/api/utils';import { ALL_THEMES } from '$lib/widgets/add-data/default-categories';import { mapServiceConfigWithDefaults, type TimeTravelMapServiceConfiguration } from '$lib/api/managers/configuration';import StringUtils from '$lib/api/utils/string.utils';import type { MapManager } from '$lib/api/map';import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model';import type { TopicManager } from '$lib/api/managers/topic';import { highlightServiceInToc } from '$lib/widgets/toc/toc.utils';
type SelectionItem = { record: ApiCswRecord; category: string | undefined; topCategory: string | undefined;};
export class AddData { private _selectedCswRecords = $state<SelectionItem[]>([]); private _selectedTimeTravelConfigs = $state<TimeTravelMapServiceConfiguration[]>([]); private _currentInfoOpened = $state<string | undefined>(); private _currentScreen = $state<AddDataCurrentScreen>(AddDataCurrentScreen.MAIN); private _loading = $state<boolean>(false); private _currentSearchText = $state<string>(''); private _currentSearchResults = $state<ApiCswRecord[]>([]); private categoriesToRecordsCache = new Map<string, Map<string, ApiCswRecord[]>>();
public currentResultsSearchText = $state<string>(''); public currentSort = $state<SearchSort>(SearchSort.PERTINENCE); public currentFilter = $state<string>(ALL_THEMES); public sortedResults = $derived.by(() => { let filtered = this._currentSearchResults; if (this.currentFilter != ALL_THEMES) { filtered = filtered.filter((x) => x.geoportailThemes.indexOf(this.currentFilter) > -1); } switch (this.currentSort) { case SearchSort.ALPHABETIC: return sort(filtered, 'title', 'asc'); case SearchSort.REVERSE_ALPHABETIC: return sort(filtered, 'title', 'desc'); case SearchSort.PERTINENCE: return sort(filtered, 'score', 'desc'); } });
constructor( private metawalApiParams: AddDataMetawalApiParams, private widgetManager: WidgetManager, private mapManager: MapManager, private fullConfig: AddDataConfig, private topic: TopicManager, ) {}
public addSelectedData() { this._selectedCswRecords.forEach((item) => { const record = item.record; const mapServiceUrl = record.mapServiceUrl; if (mapServiceUrl && record.mapServiceType) { try { const mapServiceConfig = mapServiceConfigWithDefaults({ id: record.identifier ?? StringUtils.uuid(), type: record.mapServiceType, label: record.title ?? '', url: mapServiceUrl, visible: true, identifiable: true, opacity: 1, description: record.description, metadataUrl: getGeoportailUrlFromUuid(record.identifier), }); const mapService = this.mapManager.addMapService(mapServiceConfig); if (mapService && this.fullConfig.config.openTocOnMapServiceAdded) { highlightServiceInToc(mapService.id, this.widgetManager, this.mapManager); } if (item.category && item.topCategory) { this.topic.publish({ type: 'AddData-add-mapService-from-categories', layer: mapService, category: item.category, topCategory: item.topCategory, }); } } catch (error) { console.log( `Error while adding map service ${record.identifier} (${record.mapServiceUrl}) on map`, error, ); } } }); this._selectedTimeTravelConfigs.forEach((config) => { try { const mapService = this.mapManager.addMapService(config); if (mapService && this.fullConfig.config.openTocOnMapServiceAdded) { highlightServiceInToc(mapService.id, this.widgetManager, this.mapManager); } } catch (error) { console.log(`Error while adding map service ${config.id} on map`, error); } }); this.emptySelection(); if (this.closeOnDataAdded) { this.closeWidget(); } }
public searchRecordsByCategory(category: AddDataCategory, subCategory: AddDataCategory) { if (subCategory.code) { let cache = this.getSubcategoryCache(category.code, subCategory.code); if (!cache) { this._loading = true; this.mapManager.services.metawal .getRecordsBySubjects([category.code, subCategory.code]) .then((res) => { this._currentSearchResults = res; cache = res; this.setSubcategoryCache(category.code, subCategory.code, res); }) .catch((error) => console.log(`Error while loading map service for category ${subCategory}`, error)) .finally(() => (this._loading = false)); } else { this._currentSearchResults = cache; this._loading = false; } } }
public searchRecordsByTitle() { this.navigate(AddDataCurrentScreen.SEARCH_RESULT); this._loading = true; this.currentResultsSearchText = this.currentSearchText; this.mapManager.services.metawal.getRecordsByTitle(this.currentSearchText).then( (res) => { this._currentSearchResults = res; this.navigate(AddDataCurrentScreen.SEARCH_RESULT); this._loading = false; }, (err) => { this._loading = false; throw new GeoviewerError(`Error while loading map service for title ${this.currentResultsSearchText}`, { cause: err, }); }, ); }
public get currentInfoOpened(): string | undefined { return this._currentInfoOpened; }
public set currentInfoOpened(value: string) { this._currentInfoOpened = value; }
public emptySelection() { this._selectedCswRecords = []; this._selectedTimeTravelConfigs = []; }
public getSelectedCswRecordsCount(): number { return this._selectedCswRecords.length + this._selectedTimeTravelConfigs.length; }
public selectionIsEmpty(): boolean { return ( (!this._selectedCswRecords || this._selectedCswRecords.length === 0) && (!this._selectedTimeTravelConfigs || this._selectedTimeTravelConfigs.length === 0) ); }
get selectedCswRecords(): SelectionItem[] { return this._selectedCswRecords; }
public toggleRecord(result: SelectionItem, toggle: boolean): void { if (toggle) { this._selectedCswRecords.push(result); } else { this._selectedCswRecords = this._selectedCswRecords.filter( (x) => x.record.identifier !== result.record.identifier, ); } }
public toggleTimeTravelConfig(timeTravelConfig: TimeTravelMapServiceConfiguration, toggle: boolean): void { if (toggle) { this._selectedTimeTravelConfigs.push(timeTravelConfig); } else { this._selectedTimeTravelConfigs = this._selectedTimeTravelConfigs.filter( (x) => x.id != timeTravelConfig.id, ); } }
get selectedTimeTravelConfigs(): TimeTravelMapServiceConfiguration[] { return this._selectedTimeTravelConfigs; }
public getSubcategoryCache(category: string, subCategory: string): ApiCswRecord[] | undefined { return this.categoriesToRecordsCache.get(category)?.get(subCategory); }
public setSubcategoryCache(category: string, subCategory: string, results: ApiCswRecord[]): void { if (!category || !subCategory) { return; } let categoryMap = this.categoriesToRecordsCache.get(category); if (!categoryMap) { categoryMap = new Map<string, ApiCswRecord[]>(); } let subCategoryMap = categoryMap.get(subCategory); if (!subCategoryMap) { subCategoryMap = results; } categoryMap.set(subCategory, subCategoryMap); this.categoriesToRecordsCache.set(category, categoryMap); }
public navigate(newScreen: AddDataCurrentScreen): void { this._currentScreen = newScreen; }
public closeWidget() { const reference = this.widgetManager.getReference(this.widgetId); reference.deactivate(); }
public get cswRecordDisplayField() { return this.metawalApiParams.filterBySubjectQueryParams.displayField; }
get closeOnDataAdded() { return this.fullConfig.config.closeOnMapServiceAdded; }
get widgetId() { return this.fullConfig.widgetId; }
get currentSearchText(): string { return this._currentSearchText; }
set currentSearchText(value: string) { this._currentSearchText = value; }
get currentSearchResults(): ApiCswRecord[] { return this._currentSearchResults; }
get currentScreen(): AddDataCurrentScreen { return this._currentScreen; }
get loading(): boolean { return this._loading; }}
export enum AddDataCurrentScreen { SEARCH_RESULT = 'SEARCH_RESULT', CATEGORIES = 'CATEGORIES', TIME_TRAVEL = 'TIME_TRAVEL', MAIN = 'MAIN',}
const ADD_DATA_CONTEXT_KEY = 'ADD_DATA_CONTEXT_KEY';
export function setAddDataContext(addData: AddData) { setContext(ADD_DATA_CONTEXT_KEY, addData); return getAddData();}
export function getAddData(): AddData { const addData = getContext<AddData>(ADD_DATA_CONTEXT_KEY); if (!addData) { throw new Error('AddData not found in context.'); } return addData;}packages/common/src/lib/widgets/add-data/add-data.topic.ts
import type { ApiMapService } from '$lib/api/mapservices';import type { SpatialFileType } from '$lib/widgets/add-data/add-data-from-file/add-data-from-file.model';import type { ContextConfiguration } from '$lib/api/managers/configuration';import type { ApiGeoJSONFeatureCollection } from '$lib/api/domain';
export type AddDataTopicEvent = | { type: 'AddData-add-mapService-from-categories'; layer: ApiMapService; category: string; topCategory: string; } | { type: 'AddData-add-mapService-from-populars'; layer: ApiMapService; source: 'populars' | 'trending'; } | { type: 'AddData-add-mapService-from-catalog'; layer: ApiMapService; } | { type: 'AddData-add-mapService-from-url'; layer: ApiMapService; } | { type: 'AddData-add-mapService-from-file'; layer: ApiMapService; uploadType: 'file' | 'url'; fileType: SpatialFileType; sourceUrl: string | null; features: ApiGeoJSONFeatureCollection; } | { type: 'AddData-parsing-file-error'; fileType?: SpatialFileType; errorMessage: string; } | { type: 'AddData-add-mapService-from-predefined-view'; layer: ApiMapService; context: Pick<ContextConfiguration, 'id' | 'label'>; };packages/common/src/lib/widgets/add-data/AddData.svelte
<script lang="ts"> import { Root as TabsRoot, TabsContent, TabsList, TabsTrigger } from '$lib/components/shadcn/ui/tabs'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import AddDataPresets from './add-data-presets/AddDataPresets.svelte'; import AddDataAdvancedOptions from './add-data-advanced-options/AddDataAdvancedOptions.svelte'; import AddDataPopulars from './add-data-populars/AddDataPopulars.svelte'; import RootCategoryList from './add-data-from-categories/RootCategoryList.svelte'; import AddDataSearch from './add-data-search/AddDataSearch.svelte'; import { AddData, AddDataCurrentScreen, setAddDataContext } from './add-data.svelte'; import { getWidgetManager } from '$lib/api/managers/widget'; import AddDataSearchResults from './add-data-search/AddDataSearchResults.svelte'; import AddDataBottomButtons from './card-list/AddDataBottomButtons.svelte'; import { getTopicManager } from '$lib/api/managers/topic'; import type { AddDataProps } from './add-data.declaration'; import AddDataCatalog from '$lib/widgets/add-data/add-data-catalog/AddDataCatalog.svelte';
const { fullConfig }: AddDataProps = $props(); const { config } = fullConfig;
const i18n = getI18n(fullConfig.i18n); const topic = getTopicManager(); export const addDataState = setAddDataContext( new AddData(config.metawalConfig, getWidgetManager(), getMapManager(), fullConfig, topic), );</script>
<div class="gv-w-full gv-p-4"> <AddDataSearch {fullConfig} i18nRegistry={fullConfig.i18n} /></div>
{#if addDataState.currentScreen === AddDataCurrentScreen.SEARCH_RESULT} <div class="gv-w-full gv-p-4 gv-max-h-[55vh] gv-overflow-y-auto gv-flex gv-flex-col" data-add-data-search-results> <AddDataSearchResults {fullConfig} i18nRegistry={fullConfig.i18n} /> </div> <div class="gv-p-4"> <AddDataBottomButtons selectionCount={addDataState.getSelectedCswRecordsCount()} emptyButtonLabel={i18n('EMPTY')} addButtonLabel={i18n('ADD')} onAddClicked={() => addDataState.addSelectedData()} onEmptyClicked={() => addDataState.emptySelection()} /> </div>{:else} <TabsRoot class="gv-m-6 gv-mt-0"> <TabsList variant="underline"> {#if config.visibleTabs.populars} <TabsTrigger data-test-id="AddData-Tab-Populars" value="populars" variant="underline"> {i18n('tab-populars')} </TabsTrigger> {/if} {#if config.visibleTabs.categories} <TabsTrigger value="categories" data-test-id="AddData-Tab-Categories" variant="underline"> {i18n('tab-categories')} </TabsTrigger> {/if} {#if config.visibleTabs.predefinedViews} <TabsTrigger value="predefined-views" data-test-id="AddData-Tab-PredefinedViews" variant="underline"> {i18n('tab-predefined-views')} </TabsTrigger> {/if} {#if config.visibleTabs.advancedOptions} <TabsTrigger value="advanced-options" data-test-id="AddData-Tab-AdvancedOptions" variant="underline"> {i18n('tab-advanced-options')} </TabsTrigger> {/if} {#if config.visibleTabs.catalog} <TabsTrigger value="catalog" data-test-id="AddData-Tab-Catalog" variant="underline"> {i18n('tab-catalog')} </TabsTrigger> {/if} </TabsList> <div class="gv-mt-5"> <TabsContent value="populars"> <AddDataPopulars config={fullConfig.config.addDataPopularsConfig} i18nRegistry={fullConfig.i18n} /> </TabsContent> <TabsContent value="categories"> <RootCategoryList {fullConfig} /> </TabsContent> <TabsContent value="predefined-views"> <AddDataPresets i18nRegistry={fullConfig.i18n} /> </TabsContent> <TabsContent value="advanced-options"> <AddDataAdvancedOptions config={fullConfig.config.addDataAdvancedOptionsConfig} i18nRegistry={fullConfig.i18n} /> </TabsContent> <TabsContent value="catalog"> <AddDataCatalog i18nRegistry={fullConfig.i18n} addDataConfig={fullConfig} /> </TabsContent> </div> </TabsRoot>{/if}packages/common/src/lib/widgets/add-data/card-list/AddDataBackButton.svelte
<script lang="ts"> import { ChevronLeft } from 'lucide-svelte'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { cn } from '$lib/components/shadcn/utils';
type Props = { onclick: () => void; i18nRegistry: I18nRegistry; class?: string; }; let { onclick, i18nRegistry, class: className }: Props = $props();
const i18n = getI18n(i18nRegistry ?? {});</script>
<button class={cn(className, 'gv-flex gv-font-bold')} {onclick} data-test-id="AddData-BackButton"> <ChevronLeft class="gv-size-5 gv-text-primary" /> <span>{i18n('common.back')}</span></button>packages/common/src/lib/widgets/add-data/card-list/AddDataBottomButtons.svelte
<script lang="ts"> import { Button } from '$lib/components/shadcn/ui/button'; import { getAddData } from '$lib/widgets/add-data/add-data.svelte';
type Props = { onAddClicked: () => void; onEmptyClicked: () => void; selectionCount: number; addButtonLabel: string; emptyButtonLabel: string; };
let { onAddClicked = () => {}, onEmptyClicked = () => {}, selectionCount, addButtonLabel, emptyButtonLabel, }: Props = $props();
const addDataContext = getAddData();</script>
<div class="gv-flex gv-justify-end gv-space-x-4"> {#if selectionCount > 0} <Button variant="outline" onclick={() => onEmptyClicked()} data-test-id="AddData-ClearButton" size="sm"> {emptyButtonLabel} </Button> {/if}
<Button disabled={!selectionCount || selectionCount === 0} onclick={onAddClicked} data-test-id="AddData-AddButton" size="sm" > {addButtonLabel} ({selectionCount}) </Button></div>packages/common/src/lib/widgets/add-data/card-list/card-list.model.ts
import type { Icon } from '$lib/api/icons';import type { MapServiceConfiguration } from '$lib/api/managers/configuration';import type { I18nData, I18nRegistry } from '$lib/api/managers/i18n';import type { PopularConfigItem } from '$lib/widgets/add-data/add-data-populars/add-data-populars.config';
export type CardListItem = { id: string | number; label: I18nData; icon?: Icon;};
export interface Props { items: ReadonlyArray<PopularConfigItem>; onItemClick: (value: MapServiceConfiguration) => void; i18nRegistry: I18nRegistry; class?: string; dataTestIdPrefix?: string;}packages/common/src/lib/widgets/add-data/card-list/CardItem.svelte
<script lang="ts"> import type { HTMLButtonAttributes } from 'svelte/elements'; import { type Icon } from '$lib/api/icons'; import IconComp from '$lib/components/icon/Icon.svelte'; import { getI18n, type I18nData } from '$lib/api/managers/i18n'; import { cn } from '$lib/components/shadcn/utils';
interface Props extends HTMLButtonAttributes { icon: Icon | null | undefined; iconClass?: string; label: I18nData | undefined; }
let { icon, iconClass, label, ...restProps }: Props = $props(); const i18n = getI18n();</script>
<button class="gv-flex gv-flex-col gv-items-center gv-p-1 gv-border hover:gv-bg-grey-50 hover:gv-shadow-md disabled:hover:gv-shadow-none disabled:gv-bg-muted" {...restProps}> <IconComp {icon} alt={`${i18n.translate(label)}`} class={cn('gv-h-20 gv-w-full gv-object-cover gv-mb-2', iconClass)} /> <span class="gv-text-sm gv-text-center gv-font-bold">{i18n.translate(label)}</span></button>packages/common/src/lib/widgets/add-data/card-list/CardList.svelte
<script lang="ts"> import CardItem from './CardItem.svelte'; import { cn } from '$lib/components/shadcn/utils'; import { type ApiCswRecord, MetawalApiClient } from '$lib/api/clients'; import type { MapServiceConfiguration } from '$lib/api/managers/configuration'; import { getMapManager } from '$lib/api/map'; import type { Props } from '$lib/widgets/add-data/card-list/card-list.model'; import { getI18n } from '$lib/api/managers/i18n'; import type { PopularConfigItem } from '../add-data-populars/add-data-populars.config';
let { items, onItemClick, dataTestIdPrefix = '', class: className }: Props = $props(); const i18n = getI18n(); const mapManager = getMapManager();
type MetadataPopularConfigItem = Extract<PopularConfigItem, { type: 'METADATA' }>; type DirectPopularConfigItem = Exclude<PopularConfigItem, MetadataPopularConfigItem>;
function loadGeoportailRecord(metadataId: string): Promise<ApiCswRecord | undefined> { return MetawalApiClient.getRecordsByUuid(metadataId); }
function onRecordClicked(option: MetadataPopularConfigItem, record: ApiCswRecord) { const config = mapManager.services.metawal.cswRecordToMapServiceConfig(record); if (!config.label) { config.label = option.label ?? ''; } onItemClick(config); }
function onMapServiceClicked(option: DirectPopularConfigItem) { onItemClick(option as MapServiceConfiguration); }</script>
<div class={cn('gv-grid gv-gap-3 gv-grid-cols-[repeat(auto-fill,minmax(7.5rem,1fr))] gv-auto-rows-fr', className)}> {#each items as option (option.id)} {#if option.type === 'METADATA'} {#await loadGeoportailRecord(option.metadataId)} <CardItem disabled icon={{ lucide: 'LoaderCircle' }} iconClass="gv-animate-spin" label="" /> {:then record} {#if record} <CardItem label={option.label ?? record.title} icon={record.vignetteUrl ? { url: record.vignetteUrl } : null} data-test-id={`${dataTestIdPrefix}card-list-${record.identifier}`} onclick={() => onRecordClicked(option, record)} /> {:else} <CardItem label={i18n('common.no-result')} icon={{ lucide: 'TriangleAlert' }} disabled /> {/if} {:catch} <CardItem label={i18n('common.error-occurred')} icon={{ lucide: 'TriangleAlert' }} iconClass="gv-text-destructive" disabled /> {/await} {:else} <CardItem label={option.label} icon={option.icon} data-test-id={`${dataTestIdPrefix}card-list-${option.id}`} onclick={() => onMapServiceClicked(option)} /> {/if} {/each}</div>packages/common/src/lib/widgets/add-data/default-categories.ts
import type { AddDataCategory } from '$lib/widgets/add-data/add-data.config';
export const ALL_THEMES = 'ALL_THEMES';
export const DEFAULT_ADD_DATA_CATEGORIES: AddDataCategory[] = [ { label: { fr: 'Nature et environnement', nl: 'NL - Nature et environnement', }, code: 'Nature et environnement', icon: { lucide: 'Leaf' }, categories: [ { label: { fr: 'Faune et flore', nl: 'NL - Faune et flore', }, code: 'Faune et flore', icon: { lucide: 'Flower' }, }, { label: { fr: 'Eau', nl: 'NL - Eau', }, code: 'Eau', icon: { lucide: 'Droplets' }, }, { label: { fr: 'Sol et sous-sol', nl: 'NL - Sol et sous-sol', }, code: 'Sol et sous-sol', icon: { lucide: 'Earth' }, }, { label: { fr: 'Air', nl: 'NL - Air', }, code: 'Air', icon: { lucide: 'Fan' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Nature et environnement (autre)', icon: { lucide: 'List' }, }, ], }, { label: { fr: 'Aménagement du territoire', nl: 'NL - Aménagement du territoire', }, code: 'Aménagement du territoire', icon: { lucide: 'Building2' }, categories: [ { label: { fr: 'Plans et règlements', nl: 'NL - Plans et règlements', }, code: 'Plans et règlements', icon: { lucide: 'Map' }, }, { label: { fr: 'Risques et contraintes', nl: 'NL - Risques et contraintes', }, code: 'Risques et contraintes', icon: { lucide: 'Asterisk' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Aménagement du territoire (autre)', icon: { lucide: 'List' }, }, ], }, { label: { fr: 'Mobilité', nl: 'NL - Mobilité', }, code: 'Mobilité', icon: { lucide: 'Car' }, categories: [ { label: { fr: 'Routes', nl: 'NL - Routes', }, code: 'Routes', icon: { lucide: 'Car' }, }, { label: { fr: 'A pied et à vélo', nl: 'NL - A pied et à vélo', }, code: 'A pied et à vélo', icon: { lucide: 'Bike' }, }, { label: { fr: 'Voies navigables', nl: 'NL - Voies navigables', }, code: 'Voies navigables', icon: { lucide: 'Ship' }, }, { label: { fr: 'Transports en commun', nl: 'NL - Transports en commun', }, code: 'Transports en commun', icon: { lucide: 'BusFront' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Mobilité (autre)', icon: { lucide: 'List' }, }, ], }, { label: { fr: 'Tourisme et loisir', nl: 'NL - Tourisme et loisir', }, code: 'Tourisme et loisir', icon: { lucide: 'PersonStanding' }, categories: [ { label: { fr: 'Tourisme', nl: 'NL - Tourisme', }, code: 'Tourisme', icon: { lucide: 'Sun' }, }, { label: { fr: 'Loisirs', nl: 'NL - Loisirs', }, code: 'Loisirs', icon: { lucide: 'Tent' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Tourisme et loisir (autre)', icon: { lucide: 'List' }, }, ], }, { label: { fr: 'Données de base', nl: 'NL - Données de base', }, code: 'Données de base', icon: { lucide: 'Database' }, categories: [ { label: { fr: 'Données topographiques', nl: 'NL - Données topographiques', }, code: 'Données topographiques', icon: { lucide: 'Mountain' }, }, { label: { fr: 'Limites administratives', nl: 'NL - Limites administratives', }, code: 'Limites administratives', icon: { lucide: 'SendToBack' }, }, { label: { fr: 'Photos et imagerie', nl: 'NL - Photos et imagerie', }, code: 'Photos et imagerie', icon: { lucide: 'FileImage' }, }, { label: { fr: 'Cartes anciennes', nl: 'NL - Cartes anciennes', }, code: 'Cartes anciennes', icon: { lucide: 'ScrollText' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Données de base (autre)', icon: { lucide: 'List' }, }, ], }, { label: { fr: 'Société et activités', nl: 'NL - Société et activités', }, code: 'Société et activités', icon: { lucide: 'Plane' }, categories: [ { label: { fr: 'Industrie et services', nl: 'NL - Industrie et services', }, code: 'Industrie et services', icon: { lucide: 'Factory' }, }, { label: { fr: 'Agriculture', nl: 'NL - Agriculture', }, code: 'Agriculture', icon: { lucide: 'Tractor' }, }, { label: { fr: 'Logement et habitat', nl: 'NL - Logement et habitat', }, code: 'Logement et habitat', icon: { lucide: 'House' }, }, { label: { fr: 'Bruit', nl: 'NL - Bruit', }, code: 'Bruit', icon: { lucide: 'AudioLines' }, }, { label: { fr: 'Autres', nl: 'NL - Autres', }, code: 'Société et activités (autre)', icon: { lucide: 'List' }, }, ], },];packages/common/src/lib/widgets/add-data/default-timetravel-series.ts
// @ts-nocheck Poopiimport type { AddDataCategory } from '$lib/widgets/add-data/add-data.config';
export const DEFAULT_TIME_TRAVEL_SERIES: AddDataCategory[] = [ { label: { fr: 'Séries temporelles', nl: 'NL - Séries temporelles', }, code: 'SERIES_TEMPORELLES', icon: { lucide: 'History' }, timeTravelConfigs: [ { id: 'TIME_AFTER_1971', type: 'TIME_TRAVEL', label: 'Photos aériennes à partir de 1971', visible: true, mapServices: [ { id: 'ORTHO_1971', label: 'Orthophotos 1971', shortLabel: '1971', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_1971/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/3059d00d-9666-4a90-b606-88bbd861dd83.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1971.JPG', selected: true, }, { id: 'ORTHO_1978-1990', label: 'Orthophotos 1978-1990 (couverture partielle)', shortLabel: '1978 - 1990', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_1978_1990/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/c7c0fbdf-8ad4-4242-90bd-0afd1dde4a36.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1978.JPG', }, { id: 'ORTHO_1994-2000', label: 'Orthophotos 1994-2000', shortLabel: '1994 - 2000', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_1994_2000/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/899d2df8-a16d-4798-acc4-19d3fd1a5e20.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1994.JPG', }, { id: 'ORTHO_2001-2003', label: 'Orthophotos 2001-2003 panchromatiques', shortLabel: '2001 - 2003', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2001_2003/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/b2471828-29e4-4ba5-b6c2-6dfd8b5d3c10.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2001.JPG', }, { id: 'ORTHO_2006-2007', label: 'Orthophotos 2006-2007', shortLabel: '2006 - 2007', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2006_2007/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/e26fe111-7d7d-434e-bca1-71f7b5d0e4fb.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2006.JPG', }, { id: 'ORTHO_2009-2010', label: 'Orthophotos 2009-2010', shortLabel: '2009 - 2010', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2009_2010/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/034a86cd-3879-4ed5-8e56-28e301acc86b.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2009.JPG', }, { id: 'ORTHO_2012-2013', label: 'Orthophotos 2012-2013', shortLabel: '2012 - 2013', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2012_2013/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/f16124b7-41ed-42fe-9442-73b32708d60a.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2012.JPG', }, { id: 'ORTHO_2015', label: 'Orthophotos 2015', shortLabel: '2015', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2015/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/e5e03556-80b2-4e80-86c6-6e70ae8de191.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2015.JPG', }, { id: 'ORTHO_2016', label: 'Orthophotos 2016', shortLabel: '2016', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2016/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/647e383d-c74b-4ee6-bf48-a5ebc746e8bf.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2016.JPG', }, { id: 'ORTHO_2017', label: 'Orthophotos 2017 (couverture partielle)', shortLabel: '2017', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2017/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/19ac3075-d8cf-45d7-9a62-c6f6a3e44eae.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2017.JPG', }, { id: 'ORTHO_2018', label: 'Orthophotos 2018', shortLabel: '2018', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2018/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/71cb59f2-fb18-41bc-9dbf-00ab93f69850.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2018.JPG', }, { id: 'ORTHO_2019', label: 'Orthophotos 2019', shortLabel: '2019', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2019/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/a4c49df8-8e51-4ec2-9be0-9186cb499236.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2019.JPG', }, { id: 'ORTHO_2020', label: 'Orthophotos 2020', shortLabel: '2020', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2020/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/7369222c-5241-452a-af07-4929506212f9.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2020.JPG', }, { id: 'ORTHO_2021', label: 'Orthophotos 2021', shortLabel: '2021', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2021/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/7608c4c6-1434-4291-940c-8b9c8da64484.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2021.JPG', }, { id: 'ORTHO_2022_printemps', label: 'Orthophotos 2022 Printemps', shortLabel: '2022 prin.', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2022_PRINTEMPS/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/65a07fc7-b1d8-4bb3-b5c6-d0019a782097.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2022.JPG', }, { id: 'ORTHO_2022_ete', label: 'Orthophotos 2022 Été', shortLabel: '2022 été', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2022_ETE/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/e3d7ba0d-751e-4dd8-9b06-2ffcce550b0f.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2022.JPG', }, { id: 'ORTHO_2023', label: 'Orthophotos 2023 Été', shortLabel: '2023', url: 'https://geoservices.wallonie.be/arcgis/rest/services/IMAGERIE/ORTHO_2023_ETE/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/ad55c2ce-62ad-4c3c-b3cf-8fbc270a6b6e.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/2023.JPG', }, ], }, { id: 'TIME_BEFORE_1971', type: 'TIME_TRAVEL', label: 'Cartes anciennes 1770 - 1880', visible: true, mapServices: [ { id: 'FERRARI_1770', label: 'Cartes de Ferraris (1770-1778)', shortLabel: '1770 - 1778', url: 'https://geoservices.wallonie.be/arcgis/rest/services/CARTES_ANCIENNES/FERRARIS/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/b8b9e555-a4d1-49bf-940d-31bbbf7613fc.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1770.JPG', selected: true, }, { id: 'VANDERMAELEN_1846', label: 'Cartes de Vandermaelen (1846-1854)', shortLabel: '1846 - 1854', url: 'https://geoservices.wallonie.be/arcgis/rest/services/CARTES_ANCIENNES/VDML/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/67ed5145-72be-499b-8a95-c94711f344f1.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1846.JPG', selected: true, }, { id: 'DEPOT_GUERRE_1865', label: 'Carte du dépôt de la guerre (1865 - 1880)', shortLabel: '1865 - 1880', url: 'https://geoservices.wallonie.be/arcgis/rest/services/CARTES_ANCIENNES/DEPOT_GUERRE_1865_1880/MapServer', metadataUrl: 'https://geoportail.wallonie.be/catalogue/2005026d-c9e0-41c4-81f2-758ee21d47af.html', type: 'ARCGIS_DYNAMIC', imageUrl: 'https://geoservices.test.wallonie.be/geoviewer-info/ressources/images/thumbnails/time_travel/1865.JPG', selected: true, }, ], }, ], },];