Skip to content

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.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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

packages/common/src/lib/widgets/add-data/default-timetravel-series.ts
// @ts-nocheck Poopi
import 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,
},
],
},
],
},
];

Aller plus loin