Skip to content

Source AdvancedAddressSearch

Source AdvancedAddressSearch

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-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/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/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}
/>

Aller plus loin