Skip to content

Source AdvancedSearch

Source AdvancedSearch

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/advanced-search/advanced-search.declaration.ts

packages/common/src/lib/widgets/advanced-search/advanced-search.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';
import { type AdvancedSearchFullConfig, advancedSearchFullConfigSchema } from './advanced-search.config';
import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = {
factory: () => import('./AdvancedSearch.svelte').then((AdvancedSearch) => widgetFactorySvelte(AdvancedSearch)),
schema: () => advancedSearchFullConfigSchema,
} satisfies WidgetDeclaration;
export type AdvancedSearchProps = WidgetProps<AdvancedSearchFullConfig>;

packages/common/src/lib/widgets/advanced-search/advanced-search.config.ts

packages/common/src/lib/widgets/advanced-search/advanced-search.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration';
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';
import { i18nSchemaFrom } from '$lib/api/managers/i18n';
import { z } from 'zod';
import { advancedCadSearchConfigSchema } from './advanced-cad-search/advanced-cad-search.config';
import { coordinateSearchConfigLight } from './advanced-coordinate-search/advanced-coordinate-search.config';
import { advancedSegmentationSearchConfigSchema } from './advanced-segmentation-search/advanced-segmentation-search.config';
import { iconSchema } from '$lib/api/icons';
import {
advancedSearchAddressConfigSchema,
searchAddressTranslations,
} from '$lib/widgets/advanced-search/advanced-address-search/advanced-address-search.config';
const searchConfigSchema = z.object({
type: z.enum(['CAD', 'SEG', 'COOR', 'ADR']),
icon: iconSchema,
});
export type SearchOptionConfig = z.infer<typeof searchConfigSchema>;
export const advancedSearchConfigSchema = z.object({
availableSearches: z.array(searchConfigSchema).default([
{ type: 'ADR', icon: { lucide: 'House' } },
{ type: 'CAD', icon: { geoviewer: 'parcel' } },
{ type: 'SEG', icon: { geoviewer: 'road' } },
{ type: 'COOR', icon: { lucide: 'LocateFixed' } },
]),
cadSearchConfig: advancedCadSearchConfigSchema.prefault({}),
coordinateSearchConfig: coordinateSearchConfigLight.prefault({}),
segmentationSearchConfig: advancedSegmentationSearchConfigSchema.prefault({}),
addressSearchConfig: advancedSearchAddressConfigSchema.prefault({}),
});
export const advancedSearchFullConfigSchema = defineWidgetConfig({
title: {
fr: 'Recherche avancée',
nl: 'NL - Recherche avancée',
},
icon: {
geoviewer: 'advanced-search',
},
inToolbar: inToolbarSchemaFrom(false),
i18n: i18nSchemaFrom({
CAD: {
fr: 'Parcelle cadastrale',
nl: 'NL - Parcelle cadastrale',
},
COOR: {
fr: 'Coordonnées',
nl: 'NL - Coordonnées',
},
SEG: {
fr: 'Route régionale',
nl: 'NL - Route régionale',
},
ADR: {
fr: 'Adresse',
nl: 'NL - Adresse',
},
...searchAddressTranslations,
}),
onActivate: {
deactivate: {
classes: ['Export', 'Report', 'Identify', 'AddData', 'Draw', 'MeasureDistance', 'MeasureSurface'],
},
},
config: advancedSearchConfigSchema.prefault({}),
});
export type AdvancedSearchFullConfig = z.infer<typeof advancedSearchFullConfigSchema>;

packages/common/src/lib/widgets/advanced-search/advanced-address-search/advanced-address-search.config.ts

packages/common/src/lib/widgets/advanced-search/advanced-address-search/advanced-address-search.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration';
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 { z } from 'zod';
export const searchAddressTranslations = {
'search-municipality-label': {
fr: 'Commune / Code postal',
nl: 'NL - Commune / Code postal',
},
'search-street-label': {
fr: 'Rue',
nl: 'NL - Rue',
},
'search-number-label': {
fr: 'Numéro',
nl: 'NL - Numéro',
},
'i-go': {
fr: "J'y vais",
nl: 'Ik ga',
},
};
export const advancedsearchAddressConfigSchema = z
.object({
symbolConfig: apiFeatureSymbolsSchema.prefault({}),
})
.prefault({});
export const advancedSearchAddressConfigSchema = z.object({
config: advancedsearchAddressConfigSchema.prefault({}),
i18n: i18nSchemaFrom(searchAddressTranslations),
});
export const advancedSearchAddressFullConfigSchema = defineWidgetConfig({
title: {
fr: 'Rechercher une adresse',
nl: 'NL - Rechercher une adresse',
},
icon: {
lucide: 'MapPinned',
},
inToolbar: inToolbarSchemaFrom({
type: 'button',
}),
i18n: i18nSchemaFrom(searchAddressTranslations),
config: advancedsearchAddressConfigSchema,
});
export type AdvancedSearchAddressFullConfig = z.infer<typeof advancedSearchAddressFullConfigSchema>;

packages/common/src/lib/widgets/advanced-search/advanced-address-search/advanced-address-search.declaration.ts

packages/common/src/lib/widgets/advanced-search/advanced-address-search/advanced-address-search.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';
import {
type AdvancedSearchAddressFullConfig,
advancedSearchAddressFullConfigSchema,
} from './advanced-address-search.config';
import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = {
factory: () =>
import('./AdvancedAddressSearch.svelte').then((AdvancedAddressSearch) =>
widgetFactorySvelte(AdvancedAddressSearch),
),
schema: () => advancedSearchAddressFullConfigSchema,
} satisfies WidgetDeclaration;
export type AdvancedAddressSearchProps = WidgetProps<AdvancedSearchAddressFullConfig>;

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressCommuneCombobox.svelte

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressCommuneCombobox.svelte
<script lang="ts">
import { type GeolisterResponsePosition, IcarGeolocalisationService } from '$lib/api/clients/geocodews';
import { debounceState, derivedWithDestroy, queryState } from '$lib/api/utils';
import { ApiCombobox, type ComboboxSelectedItem } from '$lib/components/api-combobox';
interface CommuneValue {
cp?: string;
ins?: string;
}
interface Props {
selectedCommune?: string;
}
let { selectedCommune = $bindable() }: Props = $props();
let filterText = $state('');
const debouncedFilter = debounceState(() => filterText, 500);
const filter = $derived(debouncedFilter.debounced?.length > 2 ? debouncedFilter.debounced : null);
const restQueryClient = derivedWithDestroy(
() => searchCommune(filter),
(query) => query?.cancel(),
);
const searchQuery = queryState({
queryFn: ({ query }) => query!.then(mapToSelectItem),
inputFn: () => ({ query: restQueryClient.value }),
disabled: () => !restQueryClient.value,
});
$effect(() => {
if (!selectedCommune) {
filterText = '';
searchQuery.data = [];
}
});
// We need to be able to search commune based on the INS or the CP. Thus, we need an internal
// object to store both those params and filter on them.
// As the second endpoint of the AdvancedAddressSearch needs the INS, we set the "external" value to
// the INS.
let internalSelectedCommune = $state<CommuneValue | undefined>();
$effect(() => {
if (!internalSelectedCommune) {
selectedCommune = undefined;
}
if (internalSelectedCommune && internalSelectedCommune.ins) {
selectedCommune = internalSelectedCommune.ins;
}
});
function searchCommune(filter: string | null) {
if (!filter) {
return null;
}
// Si l'utilisateur a encodé un code postal, le paramètre à envoyer au service est différent
const cp = parseInt(filter);
if (isNaN(cp)) {
return IcarGeolocalisationService.geolistAsJson({ city: filter });
} else {
return IcarGeolocalisationService.geolistAsJson({ zone: filter });
}
}
function filterCommunes(filterText: string, options: ComboboxSelectedItem<CommuneValue | undefined>[]) {
const searchText = filterText.toLowerCase();
return options.filter((x) => {
return (
(x.label && x.label.toLowerCase().indexOf(searchText) > -1) ||
(x.value && x.value.ins && x.value.ins.indexOf(searchText) > -1) ||
(x.value && x.value.cp && x.value.cp.indexOf(searchText) > -1)
);
});
}
function mapToSelectItem(res: GeolisterResponsePosition) {
if (res.candidates) {
return res.candidates
.filter((x) => x.city != undefined)
.reduce(
(acc, candidate) => {
const zone = candidate.zone;
const city = candidate.city;
// As the second endpoint of the AdvancedAddressSearch needs an INS, we filter out all
// values that do not provide an ins
if (!city || !city.name || !city.ident) return acc;
let name = city.name;
const value: CommuneValue = {
cp: undefined,
ins: undefined,
};
// If the candidate as a zone (object where the CP is), we add it to the value
if (zone) {
value.cp = zone.ident;
}
value.ins = city.ident;
if (!acc.set.has(value.ins)) {
acc.set.add(value.ins);
acc.list.push({
label: name!,
value,
});
}
return acc;
},
{ list: [] as ComboboxSelectedItem<CommuneValue>[], set: new Set<string>() },
).list;
}
return [];
}
</script>
<ApiCombobox
bind:value={internalSelectedCommune}
bind:filterText
filterFunction={filterCommunes}
loading={searchQuery.loading}
lazy
options={searchQuery.data}
/>

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressNumeroCombobox.svelte

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressNumeroCombobox.svelte
<script lang="ts">
import { type GeolisterResponsePosition, IcarGeolocalisationService } from '$lib/api/clients/geocodews';
import { debounceState, derivedWithDestroy, queryState } from '$lib/api/utils';
import { ApiCombobox, type ComboboxSelectedItem } from '$lib/components/api-combobox';
interface Props {
selectedStreet?: string;
selectedNumber?: string;
}
let { selectedStreet, selectedNumber = $bindable() }: Props = $props();
let filterText = $state('');
const debouncedFilter = debounceState(() => filterText, 500);
const filter = $derived.by(() => {
const value = debouncedFilter.debounced;
return value && value.length > 0 ? value : '*';
});
const disabled = $derived.by(() => !selectedStreet || !searchQuery || !searchQuery.data);
$effect(() => {
if (!selectedNumber) {
filterText = '';
searchQuery.data = [];
}
});
$effect(() => {
if (selectedStreet || !selectedStreet) {
selectedNumber = undefined;
}
});
const restQueryClient = derivedWithDestroy(
() =>
filter && selectedStreet
? IcarGeolocalisationService.geolistAsJson({ street: selectedStreet, house: filter })
: null,
(query) => query?.cancel(),
);
const searchQuery = queryState({
queryFn: ({ query }) => query!.then(mapToSelectItem),
inputFn: () => ({ query: restQueryClient.value }),
disabled: () => !restQueryClient.value,
});
function mapToSelectItem(res: GeolisterResponsePosition) {
if (res.candidates) {
return res.candidates
.filter((x) => x.house !== undefined)
.reduce(
(acc, candidate) => {
if (candidate.house) {
const { ident, name } = candidate.house;
if (ident && !acc.set.has(ident)) {
acc.set.add(ident);
acc.list.push({
label: name!,
value: ident?.toString(),
});
}
}
return acc;
},
{ list: [] as ComboboxSelectedItem<string>[], set: new Set<number>() },
).list;
}
return [];
}
</script>
<ApiCombobox
bind:value={selectedNumber}
bind:filterText
lazy
loading={searchQuery.loading}
options={searchQuery.data}
{disabled}
/>

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressSearch.svelte
<script lang="ts">
import { IcarGeolocalisationService } from '$lib/api/clients/geocodews';
import type { ApiGeoJSON } from '$lib/api/domain/api-geojson.model';
import { getI18n } from '$lib/api/managers/i18n';
import { getMapManager } from '$lib/api/map';
import { Button } from '$lib/components/shadcn/ui/button';
import { Label } from '$lib/components/shadcn/ui/label';
import { onDestroy } from 'svelte';
import Loader from '$lib/components/common/Loader.svelte';
import CommuneCombobox from './AdvancedAddressCommuneCombobox.svelte';
import NumeroCombobox from './AdvancedAddressNumeroCombobox.svelte';
import StreetCombobox from './AdvancedAddressStreetCombobox.svelte';
import type { ApiFeature } from '$lib/api/feature';
import type { AdvancedAddressSearchProps } from './advanced-address-search.declaration';
import StringUtils from '$lib/api/utils/string.utils';
let { fullConfig }: AdvancedAddressSearchProps = $props();
const i18n = getI18n(fullConfig.i18n);
const mapManager = getMapManager();
let loading = $state(false);
let selectedCommune = $state<string | undefined>(undefined);
let selectedStreet = $state<string | undefined>(undefined);
let selectedNumber = $state<string | undefined>(undefined);
let currentFeature: ApiFeature | undefined;
let oldFeature: ApiFeature | undefined;
onDestroy(() => {
if (oldFeature) {
mapManager.tools.highlight.unhighlightFeature(oldFeature);
}
if (currentFeature) {
mapManager.tools.highlight.unhighlightFeature(currentFeature);
}
});
function search() {
loading = true;
const number = StringUtils.isNullOrEmpty(selectedNumber) ? undefined : selectedNumber;
const street = number != undefined || StringUtils.isNullOrEmpty(selectedStreet) ? undefined : selectedStreet;
const commune =
number != undefined || street != undefined || StringUtils.isNullOrEmpty(selectedCommune)
? undefined
: selectedCommune;
IcarGeolocalisationService.geolistAsJson({
city: commune,
street: street,
house: number,
geom: true,
}).then(
(res) => {
oldFeature = currentFeature;
if (res.candidates && res.candidates.length > 0) {
const bestResult = res.candidates.sort((a, b) => (a.score && b.score ? b.score - a.score : 0))[0];
const bestResultGeometry =
bestResult.house?.geometry ?? bestResult.street?.geometry ?? bestResult.city?.geometry;
handleHighlight(bestResultGeometry as ApiGeoJSON);
}
loading = false;
},
(err) => {
console.error(err);
loading = false;
},
);
}
function handleHighlight(geoJSONGeometry: ApiGeoJSON | undefined) {
oldFeature = currentFeature;
if (geoJSONGeometry) {
currentFeature = mapManager.tools.featureConverter.geoJSON.fromGeoJSON(geoJSONGeometry)[0];
mapManager.tools.highlight.highlightFeature(currentFeature);
} else {
currentFeature = undefined;
}
if (oldFeature) {
mapManager.tools.highlight.unhighlightFeature(oldFeature);
}
if (currentFeature) {
mapManager.tools.zoom.zoomToFeature(currentFeature);
}
}
function reset() {
selectedCommune = undefined;
selectedStreet = undefined;
selectedNumber = undefined;
if (currentFeature) {
mapManager.tools.highlight.unhighlightFeature(currentFeature);
}
}
</script>
<div class="gv-space-y-3">
<div>
<Label class="gv-font-bold">{i18n('search-municipality-label')}</Label>
<CommuneCombobox bind:selectedCommune />
</div>
<div>
<Label class="gv-font-bold">{i18n('search-street-label')}</Label>
<StreetCombobox {selectedCommune} bind:selectedStreet />
</div>
<div>
<Label class="gv-font-bold">{i18n('search-number-label')}</Label>
<NumeroCombobox {selectedStreet} bind:selectedNumber />
</div>
{#if loading}
<div class="gv-p-4 gv-flex gv-justify-center gv-items-center">
<Loader />
</div>
{/if}
<div class="gv-pt-5">
<Button disabled={!selectedCommune} onclick={() => search()} size="sm">{i18n('i-go')}</Button>
<Button variant="secondary" onclick={() => reset()} size="sm">{i18n('common.reset')}</Button>
</div>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressStreetCombobox.svelte

packages/common/src/lib/widgets/advanced-search/advanced-address-search/AdvancedAddressStreetCombobox.svelte
<script lang="ts">
import { type GeolisterResponsePosition, IcarGeolocalisationService } from '$lib/api/clients/geocodews';
import { debounceState, derivedWithDestroy, queryState } from '$lib/api/utils';
import { ApiCombobox, type ComboboxSelectedItem } from '$lib/components/api-combobox';
interface Props {
selectedStreet?: string;
selectedCommune?: string;
}
let { selectedStreet = $bindable(), selectedCommune }: Props = $props();
let filterText = $state('');
const debouncedFilter = debounceState(() => filterText, 500);
const filter = $derived(debouncedFilter.debounced?.length > 2 ? debouncedFilter.debounced : null);
const disabled = $derived.by(() => !selectedCommune || !searchQuery || !searchQuery.data);
const restQueryClient = derivedWithDestroy(
() => (filter ? IcarGeolocalisationService.geolistAsJson({ city: selectedCommune, street: filter }) : null),
(query) => query?.cancel(),
);
$effect(() => {
if (!selectedStreet) {
filterText = '';
searchQuery.data = [];
}
});
// If the commune changes, selected street has to be reset
$effect(() => {
if (selectedCommune) {
selectedStreet = undefined;
}
});
const searchQuery = queryState({
queryFn: ({ query }) => query!.then(mapToSelectItem),
inputFn: () => ({ query: restQueryClient.value }),
disabled: () => !restQueryClient.value,
});
function mapToSelectItem(res: GeolisterResponsePosition) {
if (res.candidates) {
return res.candidates
.filter((x) => x.street != undefined)
.reduce(
(acc, candidate) => {
if (candidate.street) {
const { ident, name } = candidate.street;
if (ident && !acc.set.has(ident)) {
acc.set.add(ident);
acc.list.push({
label: name!,
value: ident?.toString(),
});
}
}
return acc;
},
{ list: [] as ComboboxSelectedItem<string>[], set: new Set<number>() },
).list;
}
return [];
}
</script>
<ApiCombobox
bind:value={selectedStreet}
bind:filterText
lazy
loading={searchQuery.loading}
options={searchQuery.data}
{disabled}
/>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte.ts

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte.ts
import { ApiParcelWsClient, CadmapControllerService } from '$lib/api/clients';
import type { I18nRegistry } from '$lib/api/managers/i18n';
import { extractCapakeyComponents, formatCapakey } from '$lib/api/utils';
import { getContext, setContext } from 'svelte';
import { showToast } from '$lib/components/toast/toast.utils';
import type { ApiFeature } from '$lib/api/feature';
import type { MapManager } from '$lib/api/map';
import type { ApiGeoJSONFeatureCollection } from '$lib/api/domain';
export class AdvancedCadSearchContext {
private _selectedCommune = $state<string | undefined>();
private _selectedDivision = $state<string | undefined>();
private _selectedSection = $state<string | undefined>();
private _selectedRadical = $state<string | undefined>();
private _selectedExposant = $state<string | undefined>();
private _selectedPuissance = $state<string | undefined>();
private _selectedBis = $state<string | undefined>();
private _capakey = $state<string>('');
private onMapFeature: ApiFeature | undefined;
private readonly COMPLETE_CAPAKEY_LENGTH = 17;
constructor(
public i18nStore: I18nRegistry,
private mapManager: MapManager,
private capakeyKeyNotCompleteMessage: string,
) {
$effect.root(() => {
$effect(() => {
const anyChange = `${this._selectedCommune}${this._selectedDivision}${this._selectedSection}${this._selectedRadical}${this._selectedExposant}${this._selectedPuissance}${this._selectedBis}`;
this.buildCapakey();
});
});
}
public removeOnMapFeature() {
if (this.onMapFeature) {
this.mapManager.tools.highlight.unhighlightFeature(this.onMapFeature);
this.onMapFeature = undefined;
}
}
public search() {
if (!this.capakeyIsComplete && !this.selectedCommune) {
showToast({ level: 'info', message: this.capakeyKeyNotCompleteMessage });
return;
}
this.removeOnMapFeature();
this.getSearchResult().then((geoJson) => {
if (!geoJson) {
return;
}
const resultFeatures = this.mapManager.tools.featureConverter.geoJSON.fromGeoJSON(geoJson) ?? [];
this.onMapFeature = this.mapManager.tools.geometryEngine.union(resultFeatures);
this.mapManager.tools.highlight.highlightFeature(this.onMapFeature);
this.mapManager.tools.zoom.zoomToFeature(this.onMapFeature);
});
}
public async getSearchResult(): Promise<ApiGeoJSONFeatureCollection | undefined> {
if (!this._selectedCommune && this._capakey && this._capakey.length !== 0) {
return ApiParcelWsClient.getShapeFromCapakey(this._capakey);
}
const {
_selectedCommune,
_selectedDivision,
_selectedSection,
_selectedRadical,
_selectedExposant,
_selectedPuissance,
_selectedBis,
} = this;
if (_selectedDivision) {
if (_selectedSection) {
if (_selectedRadical) {
if (_selectedExposant) {
if (_selectedPuissance) {
if (_selectedBis) {
return ApiParcelWsClient.getParcelleShapesByBis(
_selectedDivision,
_selectedSection,
_selectedRadical,
_selectedExposant,
_selectedPuissance,
_selectedBis,
);
}
return ApiParcelWsClient.getParcelleShapesByPuissance(
_selectedDivision,
_selectedSection,
_selectedRadical,
_selectedExposant,
_selectedPuissance,
);
}
return ApiParcelWsClient.getParcelleShapesByExposant(
_selectedDivision,
_selectedSection,
_selectedRadical,
_selectedExposant,
);
}
return ApiParcelWsClient.getParcelleShapesFromRadical(
_selectedDivision,
_selectedSection,
_selectedRadical,
);
}
return ApiParcelWsClient.getShapeSection(_selectedDivision, _selectedSection);
}
return ApiParcelWsClient.getShapeDivision(_selectedDivision);
}
if (_selectedCommune) {
return ApiParcelWsClient.getShapeCommune(_selectedCommune);
}
}
public get capakeyIsComplete(): boolean {
return this._capakey?.length === this.COMPLETE_CAPAKEY_LENGTH;
}
public reset(withCapakey = true): void {
this.selectedCommune = undefined;
if (withCapakey) {
this._capakey = '';
}
this.removeOnMapFeature();
}
private buildCapakey(): void {
if (!this._selectedDivision) {
return;
}
this._capakey = formatCapakey({
division: this._selectedDivision,
section: this._selectedSection,
radical: this._selectedRadical,
exposant: this._selectedExposant,
puissance: this._selectedPuissance,
bis: this._selectedBis,
});
}
set capakey(value: string) {
this._capakey = value;
if (this._capakey.length === this.COMPLETE_CAPAKEY_LENGTH) {
const capakeyComponents = extractCapakeyComponents(this._capakey);
if (!capakeyComponents) {
return;
}
const { division, radical, exposant, puissance, bis, section } = capakeyComponents;
CadmapControllerService.getShapeParcellesByBis({
codeDiv: division,
sect: section,
radical: radical,
exposant: exposant,
puissance: puissance,
bis: bis,
}).then((res) => {
const selectedIns = res[0]?.commune;
if (selectedIns) {
this.selectedCommune = selectedIns;
this.selectedDivision = division;
this.selectedSection = section;
this.selectedRadical = radical;
this.selectedExposant = exposant;
this.selectedPuissance = puissance;
this.selectedBis = bis;
}
});
}
}
set selectedCommune(value: string | undefined) {
if (this._selectedCommune != value) {
this.selectedDivision = undefined;
}
this._selectedCommune = value;
}
set selectedDivision(value: string | undefined) {
if (this._selectedDivision != value) {
this.selectedSection = undefined;
}
this._selectedDivision = value;
}
set selectedSection(value: string | undefined) {
if (this._selectedSection != value) {
this.selectedRadical = undefined;
}
this._selectedSection = value;
}
set selectedRadical(value: string | undefined) {
if (this._selectedRadical != value) {
this.selectedExposant = undefined;
}
this._selectedRadical = value;
}
set selectedExposant(value: string | undefined) {
if (this._selectedExposant != value) {
this.selectedPuissance = undefined;
}
this._selectedExposant = value;
}
set selectedPuissance(value: string | undefined) {
if (this._selectedPuissance != value) {
this.selectedBis = undefined;
}
this._selectedPuissance = value;
}
set selectedBis(value: string | undefined) {
this._selectedBis = value;
}
get selectedCommune(): string | undefined {
return this._selectedCommune;
}
get selectedDivision(): string | undefined {
return this._selectedDivision;
}
get selectedSection(): string | undefined {
return this._selectedSection;
}
get selectedRadical(): string | undefined {
return this._selectedRadical;
}
get selectedExposant(): string | undefined {
return this._selectedExposant;
}
get selectedPuissance(): string | undefined {
return this._selectedPuissance;
}
get selectedBis(): string | undefined {
return this._selectedBis;
}
get capakey(): string {
return this._capakey;
}
}
const ADVANCED_CAD_CONTEXT_KEY = 'ADVANCED_CAD_CONTEXT_KEY';
export function setAdvancedCadContext(legendData: AdvancedCadSearchContext) {
setContext(ADVANCED_CAD_CONTEXT_KEY, legendData);
return getAdvancedCadContext();
}
export function getAdvancedCadContext(): AdvancedCadSearchContext {
const advancedCadContext = getContext<AdvancedCadSearchContext>(ADVANCED_CAD_CONTEXT_KEY);
if (!advancedCadContext) {
throw new Error('AdvancedCadContext not found in context.');
}
return advancedCadContext;
}

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search.config.ts

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration';
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';
import { i18nSchemaFrom } from '$lib/api/managers/i18n';
import { z } from 'zod';
export const advancedCadSearchTranslations = {
commune: {
fr: 'Commune',
nl: 'NL - Commune',
},
division: {
fr: 'Division',
nl: 'NL - Division',
},
section: {
fr: 'Section',
nl: 'NL - Section',
},
radical: {
fr: 'Radical',
nl: 'NL - Radical',
},
exposant: {
fr: 'Exposant',
nl: 'NL - Exposant',
},
puissance: {
fr: 'Puissance',
nl: 'NL - Puissance',
},
bis: {
fr: 'Bis',
nl: 'NL - Bis',
},
capakey: {
fr: 'Capakey',
nl: 'NL - Capakey',
},
'capakey-not-complete': {
fr: 'Veuillez indiquer une capakey complète',
nl: 'NL - Veuillez indiquer une capakey complète',
},
};
export const advancedCadSearchConfigSchema = z.object({
i18n: i18nSchemaFrom(advancedCadSearchTranslations),
});
export const advancedSearchFullConfigSchema = defineWidgetConfig({
title: {
fr: 'Recherche avancée de parcelles cadastrales',
nl: 'NL - Recherche avancée de parcelles cadastrales',
},
inToolbar: inToolbarSchemaFrom(false),
i18n: i18nSchemaFrom({}),
config: advancedCadSearchConfigSchema.optional().prefault({}),
});
export type AdvancedCadSearchFullConfig = z.infer<typeof advancedSearchFullConfigSchema>;
export type AdvancedCadSearchConfig = z.infer<typeof advancedCadSearchConfigSchema>;

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/AdvancedCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/AdvancedCadSearch.svelte
<script lang="ts">
import { Separator } from '$lib/components/shadcn/ui/separator';
import type { AdvancedCadSearchConfig } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search.config';
import {
AdvancedCadSearchContext,
setAdvancedCadContext,
} from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import CommuneCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/CommuneCadSearch.svelte';
import DivisionCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/DivisionCadSearch.svelte';
import SectionCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/SectionCadSearch.svelte';
import RadicalCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/RadicalCadSearch.svelte';
import ExposantCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/ExposantCadSearch.svelte';
import PuissanceCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/PuissanceCadSearch.svelte';
import BisCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/BisCadSearch.svelte';
import { Button } from '$lib/components/shadcn/ui/button';
import { getMapManager } from '$lib/api/map';
import RotateCcw from 'lucide-svelte/icons/rotate-ccw';
import CapakeyCadSearch from '$lib/widgets/advanced-search/advanced-cad-search/CapakeyCadSearch.svelte';
import { getI18n } from '$lib/api/managers/i18n';
import { onDestroy } from 'svelte';
type Props = {
config: AdvancedCadSearchConfig;
};
let { config }: Props = $props();
const i18n = getI18n(config.i18n);
const mapManager = getMapManager();
const advancedCadContext = setAdvancedCadContext(
new AdvancedCadSearchContext(config.i18n, mapManager, i18n('capakey-not-complete')),
);
onDestroy(() => {
advancedCadContext.removeOnMapFeature();
});
</script>
<div class="gv-space-y-3">
<CapakeyCadSearch />
<Separator />
<CommuneCadSearch />
<DivisionCadSearch />
<SectionCadSearch />
<RadicalCadSearch />
<ExposantCadSearch />
<PuissanceCadSearch />
<BisCadSearch />
<div class="gv-flex gv-justify-end gv-gap-2">
<Button
data-test-id="AdvancedSearch-Parcel-Reset"
variant="secondary"
onclick={() => advancedCadContext.reset()}
size="sm"
>
<RotateCcw class="gv-size-4" />
{i18n('common.reset')}
</Button>
<Button data-test-id="AdvancedSearch-Parcel-Search" onclick={() => advancedCadContext.search()} size="sm">
{i18n('common.search')}
</Button>
</div>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/BisCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/BisCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(
!!advancedCadContext.selectedDivision &&
!!advancedCadContext.selectedSection &&
!!advancedCadContext.selectedRadical &&
!!advancedCadContext.selectedExposant &&
!!advancedCadContext.selectedPuissance,
);
const bisQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListeBis(input),
inputFn: () => ({
codeDiv: advancedCadContext.selectedDivision!,
sect: advancedCadContext.selectedSection!,
radical: advancedCadContext.selectedRadical!,
exposant: advancedCadContext.selectedExposant!,
puissance: advancedCadContext.selectedPuissance!,
}),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
bisQueryState?.data?.map((x) => {
return {
label: x.bis,
value: x.bis,
};
}) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || bisQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('bis')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Bis"
loading={bisQueryState?.loading}
bind:value={advancedCadContext.selectedBis}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/CapakeyCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/CapakeyCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { Input } from '$lib/components/shadcn/ui/input';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { getI18n } from '$lib/api/managers/i18n';
import { onEnterPressed } from '$lib/api/utils';
const advancedCadContext = $state(getAdvancedCadContext());
const i18n = getI18n(advancedCadContext.i18nStore);
</script>
<div>
<Label class="gv-font-bold">{i18n('capakey')}</Label>
<Input
data-test-id="AdvancedSearch-Parcel-Capakey"
onkeypress={(evt) => onEnterPressed(evt, () => advancedCadContext.search())}
bind:value={advancedCadContext.capakey}
type="text"
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/CommuneCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/CommuneCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox, type ComboboxSelectedItem } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const communeQueryState = queryState({
queryKey: 'CommuneCadSearch-getListeCommunes',
queryFn: () => CadmapControllerService.getListeCommunes({}),
});
const options = $derived.by(() => {
return (
communeQueryState.data?.map((x) => {
return {
label: x.nom,
value: x.ins,
};
}) ?? []
);
});
function filterCommunes(filterText: string, options: ComboboxSelectedItem<string | undefined>[]) {
const searchText = filterText.toLowerCase();
return options.filter((x) => {
return (
(x.label && x.label.toLowerCase().indexOf(searchText) > -1) ||
(x.value && x.value.indexOf(searchText) > -1)
);
});
}
const disabled = $derived(communeQueryState.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('commune')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Commune"
loading={communeQueryState.loading}
bind:value={advancedCadContext.selectedCommune}
filterFunction={filterCommunes}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/DivisionCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/DivisionCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(!!advancedCadContext.selectedCommune);
const divisionQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListeDivisions(input),
inputFn: () => ({ ins: advancedCadContext.selectedCommune! }),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
divisionQueryState?.data?.map((x) => ({
label: x.divNom,
value: x.codeDiv,
})) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || divisionQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('division')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Division"
loading={divisionQueryState?.loading}
bind:value={advancedCadContext.selectedDivision}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/ExposantCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/ExposantCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(
!!advancedCadContext.selectedDivision &&
!!advancedCadContext.selectedSection &&
!!advancedCadContext.selectedRadical,
);
const exposantQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListeExposants(input),
inputFn: () => ({
codeDiv: advancedCadContext.selectedDivision!,
sect: advancedCadContext.selectedSection!,
radical: advancedCadContext.selectedRadical!,
}),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
exposantQueryState?.data?.map((x) => {
return {
label: x.exposant,
value: x.exposant,
};
}) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || exposantQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('exposant')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Exposant"
loading={exposantQueryState?.loading}
bind:value={advancedCadContext.selectedExposant}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/PuissanceCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/PuissanceCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(
!!advancedCadContext.selectedDivision &&
!!advancedCadContext.selectedSection &&
!!advancedCadContext.selectedRadical &&
!!advancedCadContext.selectedExposant,
);
const puissanceQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListePuissances(input),
inputFn: () => ({
codeDiv: advancedCadContext.selectedDivision!,
sect: advancedCadContext.selectedSection!,
radical: advancedCadContext.selectedRadical!,
exposant: advancedCadContext.selectedExposant!,
}),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
puissanceQueryState?.data?.map((x) => {
return {
label: x.puissance,
value: x.puissance,
};
}) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || puissanceQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('puissance')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Puissance"
loading={puissanceQueryState?.loading}
bind:value={advancedCadContext.selectedPuissance}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/RadicalCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/RadicalCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(
!!advancedCadContext.selectedDivision && !!advancedCadContext.selectedSection,
);
const radicalQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListeRadicaux(input),
inputFn: () => ({ codeDiv: advancedCadContext.selectedDivision!, sect: advancedCadContext.selectedSection! }),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
radicalQueryState?.data?.map((x) => {
return {
label: x.radical,
value: x.radical,
};
}) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || radicalQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('radical')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Radical"
loading={radicalQueryState?.loading}
bind:value={advancedCadContext.selectedRadical}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/SectionCadSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-cad-search/SectionCadSearch.svelte
<script lang="ts">
import { Label } from '$lib/components/shadcn/ui/label';
import { getAdvancedCadContext } from '$lib/widgets/advanced-search/advanced-cad-search/advanced-cad-search-context.svelte';
import { CadmapControllerService } from '$lib/api/clients';
import { getI18n } from '$lib/api/managers/i18n';
import { ApiCombobox } from '$lib/components/api-combobox';
import { queryState } from '$lib/api/utils';
const advancedCadContext = getAdvancedCadContext();
const i18n = getI18n(advancedCadContext.i18nStore);
const requiredParamsAreFullfilled = $derived(!!advancedCadContext.selectedDivision);
const sectionQueryState = queryState({
queryFn: (input) => CadmapControllerService.getListeSections(input),
inputFn: () => ({ codeDiv: advancedCadContext.selectedDivision! }),
disabled: () => !requiredParamsAreFullfilled,
});
const options = $derived.by(() => {
return (
sectionQueryState?.data?.map((x) => {
return {
label: x.sect,
value: x.sect,
};
}) ?? []
);
});
const disabled = $derived(!requiredParamsAreFullfilled || sectionQueryState?.loading || options.length === 0);
</script>
<div>
<Label class="gv-font-bold">{i18n('section')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Parcel-Section"
loading={sectionQueryState?.loading}
bind:value={advancedCadContext.selectedSection}
{options}
{disabled}
/>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-coordinate-search/advanced-coordinate-search.config.ts

packages/common/src/lib/widgets/advanced-search/advanced-coordinate-search/advanced-coordinate-search.config.ts
import { defineWidgetConfig, inToolbarSchemaFrom } from '$lib/api/managers/configuration';
import { i18nDataSchema, i18nSchemaFrom } from '$lib/api/managers/i18n';
import { z } from 'zod';
import { extentSchema } from '$lib/api/domain/api-extent.schema';
const coordinateSearchProjectionOption = z.object({
wkid: z.number(),
label: i18nDataSchema,
dms: z.boolean().optional(),
xLabel: z.string().optional(),
yLabel: z.string().optional(),
xPlaceholder: z.string().optional(),
yPlaceholder: z.string().optional(),
});
export type CoordinateSearchProjectionOption = z.infer<typeof coordinateSearchProjectionOption>;
export const advancedCoordinateSearchFullConfigSchema = defineWidgetConfig({
title: {
fr: 'Recherche avancée de coordonnées',
nl: 'NL - Recherche avancée de coordonnées',
},
inToolbar: inToolbarSchemaFrom(false),
i18n: i18nSchemaFrom({
'coordinate-label': {
fr: 'Système de coordonnées',
nl: 'NL - Système de coordonnées',
},
'parsing-error-message': {
fr: 'Le format de la coordonnée est invalide. Le format attendu est un nombre entier ou décimal.',
nl: 'NL - Le format de la coordonnée est invalide. Le format attendu est un nombre entier ou décimal.',
},
'parsing-dms-error-message': {
fr: 'Le format de la coordonnée est invalide.',
nl: 'NL - Le format de la coordonnée est invalide.',
},
'layer-label': {
fr: 'Coordonnées',
nl: 'NL - Coordonnées',
},
'keep-coordinate': {
fr: 'Conserver le résultat sur la carte',
nl: 'NL - Conserver le résultat sur la carte',
},
'outside-of-warning-extent': {
fr: 'Les coordonnées recherchées sont en dehors de la Wallonie',
nl: 'NL - Les coordonnées recherchées sont en dehors de la Wallonie',
},
}),
config: z
.object({
options: coordinateSearchProjectionOption.array().default([
{
label: 'Lambert Belge 2008',
wkid: 3812,
xPlaceholder: '763875,1',
yPlaceholder: '643212,2',
},
{
label: 'Lambert Belge 1972',
wkid: 31370,
xPlaceholder: '263873,6',
yPlaceholder: '149848,8',
},
{
label: 'WGS84 (DD)',
wkid: 4326,
xLabel: 'Longitude',
yLabel: 'Latitude',
xPlaceholder: '5,977',
yPlaceholder: '50,588',
},
{
label: 'WGS84 (DMS)',
wkid: 4326,
dms: true,
xLabel: 'Longitude Est',
yLabel: 'Latitude Nord',
xPlaceholder: '5° 58\' 36,839" E',
yPlaceholder: '50° 35\' 17,998" N',
},
]),
warningMessageExtent: extentSchema.default({
xmin: 5700,
ymin: 7600,
xmax: 315000,
ymax: 195000,
wkid: 31370,
}),
showWarningMessage: z.boolean().default(true),
})
.prefault({}),
});
export const coordinateSearchConfigLight = advancedCoordinateSearchFullConfigSchema.pick({
i18n: true,
config: true,
});
export type CoordinateSearchConfigLight = z.infer<typeof coordinateSearchConfigLight>;
export type AdvancedCoordinateSearchFullConfig = z.infer<typeof advancedCoordinateSearchFullConfigSchema>;

packages/common/src/lib/widgets/advanced-search/advanced-coordinate-search/AdvancedCoordinateSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-coordinate-search/AdvancedCoordinateSearch.svelte
<script lang="ts">
import { initGraphicMapServiceConfiguration, MapServiceTypes } from '$lib/api/managers/configuration';
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 { ApiGraphicsMapService } from '$lib/api/mapservices';
import { onEnterPressed, parseDms } from '$lib/api/utils';
import { ApiSelect, type ApiSelectItem } from '$lib/components/api-select';
import { Button } from '$lib/components/shadcn/ui/button';
import { Checkbox } from '$lib/components/shadcn/ui/checkbox';
import { Input } from '$lib/components/shadcn/ui/input';
import { Label } from '$lib/components/shadcn/ui/label';
import RotateCcw from 'lucide-svelte/icons/rotate-ccw';
import type {
CoordinateSearchConfigLight,
CoordinateSearchProjectionOption,
} from './advanced-coordinate-search.config';
import { onDestroy } from 'svelte';
import type { ApiPoint } from '$lib/api/geometry';
import { projectionManager } from '$lib/api/managers/projection';
interface Props {
fullConfig: CoordinateSearchConfigLight;
}
let { fullConfig }: Props = $props();
const { config } = fullConfig;
const i18n = getI18n(fullConfig.i18n);
const mapManager = getMapManager();
const transform = mapManager.tools.transform;
const coordinateLayerId = 'COORDINATE_LAYER_ID';
const layer = getCoordinateLayer();
let keepInLayer = $state(layer.toc.visible);
$effect(() => {
if (keepInLayer) {
layer.toc.visible = true;
} else {
layer.removeAll();
layer.toc.visible = false;
}
});
let projectionOption = $state<CoordinateSearchProjectionOption>(fullConfig.config.options[0]);
const options: ApiSelectItem<CoordinateSearchProjectionOption>[] = $derived(
fullConfig.config.options.map((o) => ({
value: o,
label: i18n.translate(o.label),
})),
);
let xValue = $state('');
let yValue = $state('');
let parsingError = $state(false);
let outsideOfWarningExtent = $state(false);
function getCoordinateLayer(): ApiGraphicsMapService {
let layer = mapManager.layerList.findById(coordinateLayerId);
if (layer && layer.type !== MapServiceTypes.GRAPHICS) {
layer = undefined;
}
if (!layer) {
const layerConfig = initGraphicMapServiceConfiguration({
id: coordinateLayerId,
label: i18n('layer-label'),
removable: false,
toc: {
visible: false,
},
});
layer = mapManager.addGraphicMapService(layerConfig);
}
return layer as ApiGraphicsMapService;
}
function reset() {
xValue = '';
yValue = '';
parsingError = false;
outsideOfWarningExtent = false;
}
function submit() {
const { x, y } = projectionOption.dms
? parseDms(xValue, yValue)
: {
x: parseFloat(xValue.replace(',', '.')),
y: parseFloat(yValue.replace(',', '.')),
};
if (Number.isNaN(x)) {
parsingError = true;
return;
}
if (Number.isNaN(y)) {
parsingError = true;
return;
}
const projection = projectionManager.getByWkid(projectionOption.wkid);
if (!projection) {
throw new GeoviewerError(`No projection found for wkid: ${projectionOption.wkid}`);
}
const point = mapManager.tools.featureFactory.createPoint({ x, y, wkid: projection.wkid });
outsideOfWarningExtent = config.showWarningMessage && pointIsOutsideWarningBbox(point);
if (!keepInLayer) {
layer.removeAll();
}
layer.addFeature(point);
mapManager.tools.zoom.zoomToFeature(point);
}
function pointIsOutsideWarningBbox(point: ApiPoint): boolean {
const projectedWarningExtent = transform.transformExtent(config.warningMessageExtent, point.wkid);
return (
point.x < projectedWarningExtent.xmin ||
point.x > projectedWarningExtent.xmax ||
point.y < projectedWarningExtent.ymin ||
point.y > projectedWarningExtent.ymax
);
}
onDestroy(() => {
if (!keepInLayer) {
mapManager.removeMapService(coordinateLayerId);
}
});
</script>
<div class="gv-space-y-3">
<div>
<Label class="gv-font-bold">{i18n('coordinate-label')}</Label>
<ApiSelect
dataTestId="AdvancedSearch-Coord-Projections"
getKey={(p: CoordinateSearchProjectionOption) => `${p.wkid}-${p.label}`}
{options}
bind:value={projectionOption}
valueChanged={() => (parsingError = false)}
/>
</div>
<div class="gv-grid gv-grid-cols-2 gv-gap-3">
<div>
<Label class="gv-font-bold">{projectionOption.xLabel ?? 'X (m)'}</Label>
<Input
data-test-id="AdvancedSearch-Coord-X"
onkeypress={(evt) => onEnterPressed(evt, () => submit())}
placeholder={projectionOption.xPlaceholder}
bind:value={xValue}
/>
</div>
<div>
<Label class="gv-font-bold">{projectionOption.yLabel ?? 'Y (m)'}</Label>
<Input
data-test-id="AdvancedSearch-Coord-Y"
onkeypress={(evt) => onEnterPressed(evt, () => submit())}
placeholder={projectionOption.yPlaceholder}
bind:value={yValue}
/>
</div>
</div>
{#if parsingError}
<span class="gv-text-destructive gv-text-sm">
{projectionOption.dms ? i18n('parsing-dms-error-message') : i18n('parsing-error-message')}
</span>
{/if}
{#if outsideOfWarningExtent}
<span class="gv-text-destructive gv-text-sm">
{i18n('outside-of-warning-extent')}
</span>
{/if}
<div class="gv-flex gv-items-center gv-gap-2">
<Checkbox
data-test-id="AdvancedSearch-Coord-KeepCoordinates"
bind:checked={keepInLayer}
id="keep-coordinates"
/>
<Label for="keep-coordinates">{i18n('keep-coordinate')}</Label>
</div>
<div class="gv-flex gv-justify-end gv-gap-2">
<Button data-test-id="AdvancedSearch-Coord-Reset" variant="secondary" onclick={reset} size="sm">
<RotateCcw class="gv-size-4" />
{i18n('common.reset')}
</Button>
<Button data-test-id="AdvancedSearch-Coord-Search" onclick={submit} size="sm">
{i18n('common.search')}
</Button>
</div>
</div>

packages/common/src/lib/widgets/advanced-search/advanced-segmentation-search/advanced-segmentation-search.config.ts

packages/common/src/lib/widgets/advanced-search/advanced-segmentation-search/advanced-segmentation-search.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/index.js';
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';
import { i18nSchemaFrom } from '$lib/api/managers/i18n';
import { z } from 'zod';
export const advancedSegmentationSearchTranslations = {
road: {
fr: 'Route',
nl: 'NL - Route',
},
'unable-to-fetch-segments': {
fr: 'Une erreur est survenue au chargement des segments',
nl: 'NL - Une erreur est survenue au chargement des segments',
},
'bk-start': {
fr: 'Borne de début (km)',
nl: 'NL - Borne de début (km)',
},
'bk-end': {
fr: 'Borne de fin (km)',
nl: 'NL - Borne de fin (km)',
},
};
export const advancedSegmentationSearchConfigSchema = z.object({
i18n: i18nSchemaFrom(advancedSegmentationSearchTranslations),
});
export const advancedSegmentationSearchFullConfigSchema = defineWidgetConfig({
title: {
fr: 'Recherche avancée de route régionale',
nl: 'NL - Recherche avancée de route régionale',
},
inToolbar: inToolbarSchemaFrom(false),
i18n: i18nSchemaFrom(advancedSegmentationSearchTranslations),
config: advancedSegmentationSearchConfigSchema.optional().prefault({}),
});
export type AdvancedSegmentationSearchFullConfig = z.infer<typeof advancedSegmentationSearchFullConfigSchema>;

packages/common/src/lib/widgets/advanced-search/advanced-segmentation-search/AdvancedSegmentationSearch.svelte

packages/common/src/lib/widgets/advanced-search/advanced-segmentation-search/AdvancedSegmentationSearch.svelte
<script lang="ts">
import { getI18n } from '$lib/api/managers/i18n';
import { getMapManager } from '$lib/api/map';
import { ApiCombobox, type ComboboxSelectedItem } from '$lib/components/api-combobox';
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 type { AdvancedSegmentationSearchFullConfig } from '$lib/widgets/advanced-search/advanced-segmentation-search/advanced-segmentation-search.config';
import RotateCcw from 'lucide-svelte/icons/rotate-ccw';
import { onDestroy } from 'svelte';
import type { ApiFeature } from '$lib/api/feature';
import type { Segment } from '$lib/api/clients/lrsws/models/Segment';
import { onEnterPressed, queryState } from '$lib/api/utils';
import { showToast } from '$lib/components/toast/toast.utils';
interface Props {
fullConfig: AdvancedSegmentationSearchFullConfig;
}
let { fullConfig }: Props = $props();
const mapManager = getMapManager();
const segmentation = mapManager.services.segmentation;
const i18n = getI18n(fullConfig.i18n);
let bkStart = $state<number>(0);
let bkEnd = $state<number>(0);
let currentFeature: ApiFeature | undefined;
let selectedRoad = $state<Segment | null>(null);
let roadsQueryState = queryState({
queryKey: `AdvancedSegmentation-getSegmentList-1`,
queryFn: () => segmentation.getSegmentList('1'),
});
const inError = $derived(roadsQueryState?.error);
$effect(() => {
if (inError) {
showToast({ level: 'error', message: i18n('unable-to-fetch-segments') });
}
});
const roadsOptions = $derived<ComboboxSelectedItem<Segment>[]>(
roadsQueryState.data?.filter((x) => !!x).map((x) => ({ label: x.simplifiedCode ?? '', value: x })) ?? [],
);
const disableSearch = $derived.by(() => !selectedRoad || (bkEnd != undefined && bkStart > bkEnd));
$effect(() => {
if (selectedRoad) {
bkStart = selectedRoad.cumulee_start ? selectedRoad.cumulee_start / 1000 : 0;
bkEnd = selectedRoad.cumulee_end ? selectedRoad.cumulee_end / 1000 : 0;
}
});
$effect(() => {
if (bkStart < 0) {
bkStart = 0;
}
if (bkEnd < 0) {
bkEnd = 0;
}
});
function reset() {
selectedRoad = null;
bkStart = 0;
bkEnd = 0;
if (currentFeature) {
mapManager.tools.highlight.unhighlightFeature(currentFeature);
}
}
function submit() {
mapManager.services.segmentation
.getSegmentWithShape({
code: selectedRoad!.code!,
cumuleeEnd: bkEnd != undefined ? bkEnd : bkStart,
cumuleeStart: bkStart != undefined ? bkStart : 0,
systemType: '1',
})
.then((segment) => {
if (currentFeature) {
mapManager.tools.highlight.unhighlightFeature(currentFeature);
}
if (segment) {
currentFeature = segment;
mapManager.tools.zoom.zoomToFeature(currentFeature);
mapManager.tools.highlight.highlightFeature(currentFeature);
}
});
}
function filterSegments(filterText: string, options: ComboboxSelectedItem<Segment | undefined>[]) {
const filter = filterText.toLowerCase();
return options.filter((x) => {
return x.label && x.label.toLowerCase().includes(filter);
});
}
onDestroy(() => {
reset();
});
</script>
<div class="gv-space-y-3">
<div>
<Label class="gv-font-bold">{i18n('road')}</Label>
<ApiCombobox
avoidCollisions={false}
dataTestId="AdvancedSearch-Segmentation-Roads"
filterFunction={filterSegments}
bind:value={selectedRoad}
options={roadsOptions}
lazy={true}
disabled={!!inError}
loading={roadsQueryState.loading}
/>
</div>
<div>
<Label class="gv-font-bold">{i18n('bk-start')}</Label>
<Input
class="gv-input-number-without-arrows"
min="0"
on:keypress={(evt) => onEnterPressed(evt, () => submit())}
data-test-id="AdvancedSearch-Segmentation-BkStart"
type="number"
bind:value={bkStart}
/>
</div>
<div>
<Label class="gv-font-bold">{i18n('bk-end')}</Label>
<Input
class="gv-input-number-without-arrows"
min="0"
on:keypress={(evt) => onEnterPressed(evt, () => submit())}
data-test-id="AdvancedSearch-Segmentation-BkEnd"
type="number"
bind:value={bkEnd}
/>
</div>
<div class="gv-flex gv-justify-end gv-gap-2">
<Button data-test-id="AdvancedSearch-Segmentation-Reset" variant="secondary" onclick={reset} size="sm">
<RotateCcw class="gv-size-4" />
{i18n('common.reset')}
</Button>
<Button data-test-id="AdvancedSearch-Segmentation-Search" disabled={disableSearch} onclick={submit} size="sm">
{i18n('common.search')}
</Button>
</div>
</div>

packages/common/src/lib/widgets/advanced-search/AdvancedSearch.svelte

packages/common/src/lib/widgets/advanced-search/AdvancedSearch.svelte
<script lang="ts">
import { getI18n } from '$lib/api/managers/i18n';
import { CollapsibleContent, CollapsibleTrigger, Root } from '$lib/components/shadcn/ui/collapsible';
import { Icon } from '$lib/components/icon';
import { cn } from '$lib/components/shadcn/utils';
import AdvancedCadSearch from './advanced-cad-search/AdvancedCadSearch.svelte';
import AdvancedCoordinateSearch from './advanced-coordinate-search/AdvancedCoordinateSearch.svelte';
import AdvancedSegmentationSearch from './advanced-segmentation-search/AdvancedSegmentationSearch.svelte';
import type { SearchOptionConfig } from './advanced-search.config';
import type { AdvancedSearchProps } from './advanced-search.declaration';
import AdvancedSearchAddress from '$lib/widgets/advanced-search/advanced-address-search/AdvancedAddressSearch.svelte';
let { fullConfig }: AdvancedSearchProps = $props();
const { config } = fullConfig;
const i18n = getI18n(fullConfig.i18n);
let openedSearch = $state<SearchOptionConfig['type']>();
</script>
{#each config.availableSearches as currentSearch}
<Root open={openedSearch === currentSearch.type} class="gv-border-b gv-px-5 gv-py-2.5 gv-space-y-2.5">
<CollapsibleTrigger
data-test-id="AdvancedSearch-Tab-{currentSearch.type}"
onclick={() => (openedSearch = openedSearch === currentSearch.type ? undefined : currentSearch.type)}
class="gv-flex gv-w-full gv-items-center gv-space-x-2 gv-font-bold"
>
<Icon icon={currentSearch.icon} class="gv-size-6 gv-text-primary" />
<span class="gv-flex-1 gv-text-left">{i18n(currentSearch.type)}</span>
<Icon
icon={{ lucide: 'ChevronDown' }}
class={cn(openedSearch === currentSearch.type && 'gv-rotate-180', 'gv-duration-150')}
/>
</CollapsibleTrigger>
<CollapsibleContent class="gv-pl-8">
{#if currentSearch.type === 'ADR'}
<AdvancedSearchAddress fullConfig={config.addressSearchConfig} />
{:else if currentSearch.type === 'CAD'}
<AdvancedCadSearch config={config.cadSearchConfig} />
{:else if currentSearch.type === 'COOR'}
<AdvancedCoordinateSearch fullConfig={config.coordinateSearchConfig} />
{:else if currentSearch.type === 'SEG'}
<AdvancedSegmentationSearch fullConfig={config.segmentationSearchConfig} />
{:else}
Not implemented yet ...
{/if}
</CollapsibleContent>
</Root>
{/each}

Aller plus loin