Skip to content

Source LightIdentify

Source LightIdentify

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/light-identify/light-identify.declaration.ts

packages/common/src/lib/widgets/light-identify/light-identify.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';
import { type LightIdentifyConfig, lightIdentifyConfigSchema } from './light-identify.config';
import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = {
factory: () => import('./LightIdentify.svelte').then((LightIdentify) => widgetFactorySvelte(LightIdentify)),
schema: () => lightIdentifyConfigSchema,
} satisfies WidgetDeclaration;
export type LightIdentifyProps = WidgetProps<LightIdentifyConfig>;

packages/common/src/lib/widgets/light-identify/light-identify.config.ts

packages/common/src/lib/widgets/light-identify/light-identify.config.ts
import { defineWidgetConfig, inToolbarSchemaFrom } from '$lib/api/managers/configuration';
import { type I18nRegistry, i18nSchemaFrom } from '$lib/api/managers/i18n';
import { z } from 'zod';
import { hiddenContainerId } from '$lib/components/containers/hidden/hidden.schema';
import { styleRecordSchema } from '$lib/api/utils';
import type { PopupPosition } from '$lib/api/managers/popup';
export const lightIdentifyTranslations = {
'no-results-text': {
fr: "Aucune information n'est disponible à cette localisation",
nl: "NL - Aucune information n'est disponible à cette localisation",
},
on: {
fr: 'sur',
nl: 'NL - sur',
},
} as const satisfies I18nRegistry;
export const lightIdentifyConfigSchema = defineWidgetConfig({
i18n: i18nSchemaFrom(lightIdentifyTranslations),
container: hiddenContainerId,
inToolbar: inToolbarSchemaFrom(false),
active: true,
config: z
.object({
tolerance: z.number().optional().default(10),
reactiveResultsOnTocUpdate: z.boolean().optional().default(false),
centerMap: z.boolean().default(true),
featureDetailsStyle: styleRecordSchema.optional().prefault({}),
popupPosition: z.custom<PopupPosition>().default('bottom-right'),
})
.optional()
.prefault({}),
});
export type LightIdentifyConfig = z.infer<typeof lightIdentifyConfigSchema>;

packages/common/src/lib/widgets/light-identify/light-identify.model.ts

packages/common/src/lib/widgets/light-identify/light-identify.model.ts
import type { ApiFeature } from '$lib/api/feature';
import type { ApiMapService } from '$lib/api/mapservices';
import type { ApiSublayer } from '$lib/api/layers';
export interface LightIdentifyResult {
mapService: ApiMapService;
layer: ApiSublayer;
feature: ApiFeature;
}

packages/common/src/lib/widgets/light-identify/LightIdentify.svelte

packages/common/src/lib/widgets/light-identify/LightIdentify.svelte
<script lang="ts">
import type { ApiPoint } from '$lib/api/geometry';
import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model';
import { getMapManager } from '$lib/api/map';
import { isIdentifiable, type ApiMapService } from '$lib/api/mapservices';
import {
type QueryState,
type IdentifyLayerResultTree,
isQueryable,
mapServiceIdentifyResultToIdentifyTree,
queryState,
} from '$lib/api/utils';
import { onDestroy } from 'svelte';
import type { Unsubscriber } from 'svelte/store';
import PopupComponent from '$lib/components/popup/PopupComponent.svelte';
import type { LightIdentifyResult } from './light-identify.model';
import FeatureDetails from '$lib/widgets/identify/map-services/FeatureDetails.svelte';
import ChevronLeft from 'lucide-svelte/icons/chevron-left';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '$lib/components/shadcn/utils';
import { getI18n } from '$lib/api/managers/i18n';
import Loader from '$lib/components/common/Loader.svelte';
import { Expand } from 'lucide-svelte';
import { Button } from '$lib/components/shadcn/ui/button';
import {
DialogClose,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
Root,
} from '$lib/components/shadcn/ui/dialog';
import { getLayoutManager } from '$lib/api/managers/layout';
import Search from 'lucide-svelte/icons/search';
import type { LightIdentifyProps } from './light-identify.declaration';
let { fullConfig }: LightIdentifyProps = $props();
const config = fullConfig.config;
const mapManager = getMapManager();
const layoutManager = getLayoutManager();
const zoom = mapManager.tools.zoom;
const i18n = getI18n(fullConfig.i18n);
const { tolerance, centerMap, popupPosition, featureDetailsStyle } = config;
let detailsDialogOpen = $state<boolean>(false);
let open = $state<boolean>(false);
let clickedPoint = $state<ApiPoint | undefined>();
let identifyResultsQueryState = $state<QueryState<LightIdentifyResult[]> | undefined>();
let currentIndex = $state<number>(0);
const results = $derived.by(() => {
return identifyResultsQueryState && identifyResultsQueryState.data ? identifyResultsQueryState.data : [];
});
const resultsLength = $derived.by(() => results.length);
const isFirst = $derived.by(() => currentIndex === 0);
const isLast = $derived.by(() => currentIndex === resultsLength - 1);
const selectedResult = $derived.by(() => results[currentIndex]);
const title = $derived.by(() =>
selectedResult
? `${selectedResult.mapService.label} (${currentIndex + 1} ${i18n('on')} ${resultsLength})`
: i18n('common.no-result'),
);
const fields = $derived.by(
() => selectedResult?.layer && (isQueryable(selectedResult.layer) ? selectedResult.layer.fields : []),
);
let onRightClickUnsubscribe: Unsubscriber = mapManager.tools.events.on('rightClick', async (point) => {
clickedPoint = point;
});
$effect(() => {
if (clickedPoint) {
identifyResultsQueryState = queryState({
queryFn: () =>
Promise.allSettled(getAllIdentifyPromises()).then((res) => {
const allResults = res.flatMap((res) => (res.status === 'fulfilled' ? res.value : []));
currentIndex = 0;
return allResults;
}),
});
openPopup();
}
});
function getAllIdentifyPromises(): Promise<LightIdentifyResult[]>[] {
const identifiableLayers = mapManager.layerList.list
.filter((l) => l.toc.visible && l.visible && isIdentifiable(l))
.toReversed();
return identifiableLayers.map((mapService) => identifyMapService(clickedPoint!, mapService));
}
function identifyMapService(clickedPoint: ApiPoint, mapService: ApiMapService): Promise<LightIdentifyResult[]> {
if (!isIdentifiable(mapService)) {
throw new GeoviewerError('Should never happen but Typescript is stubborn');
}
return mapService
.identify(clickedPoint, {
returnGeometry: true,
tolerance,
})
.then((res) => {
const identifyResultTree = mapServiceIdentifyResultToIdentifyTree({
mapService: mapService,
identifyResponse: res,
});
return identifyResultTree.children.flatMap((child) => handleIdentifyChild(child, mapService));
});
}
function handleIdentifyChild(
child: IdentifyLayerResultTree,
mapService: ApiMapService,
results: LightIdentifyResult[] = [],
): LightIdentifyResult[] {
child.features?.forEach((feature) => {
results.push({
feature,
mapService,
layer: child.layer,
});
});
child.children?.forEach((c) => handleIdentifyChild(c, mapService, results));
return results;
}
function openPopup(): void {
open = false;
setTimeout(() => {
open = true;
}, 50);
}
function nextResult() {
currentIndex += 1;
}
function previousResult() {
currentIndex -= 1;
}
onDestroy(() => {
if (onRightClickUnsubscribe) {
onRightClickUnsubscribe();
}
});
</script>
{#if clickedPoint && identifyResultsQueryState}
<PopupComponent {title} {popupPosition} {open} location={clickedPoint} {centerMap}>
<div class="gv-h-full">
{#if identifyResultsQueryState.loading}
<Loader class="gv-p-1 gv-size-8" />
{:else if selectedResult}
<div class="gv-flex gv-justify-between gv-items-center">
<div class="gv-font-bold">{selectedResult.layer.label}</div>
<div class="gv-flex gv-justify-between gv-text-primary">
<button class={cn(isFirst && 'gv-opacity-30')} disabled={isFirst}
><ChevronLeft onclick={previousResult} /></button
>
<button class={cn(isLast && 'gv-opacity-30')} disabled={isLast}
><ChevronRight onclick={nextResult} /></button
>
</div>
</div>
<FeatureDetails
containerStyleRecord={featureDetailsStyle}
zoomOnOver={false}
feature={selectedResult.feature}
{fields}
/>
<div class="gv-flex gv-justify-end gv-mt-1 gv-gap-1">
<Button onclick={() => (detailsDialogOpen = true)} variant="outline">
<Expand class="gv-size-4" />
{i18n('common.expand')}
</Button>
<Button onclick={() => zoom.zoomToFeature(selectedResult.feature)} variant="outline">
<Search class="gv-size-4" />
{i18n('common.zoom')}
</Button>
</div>
{:else}
{i18n('no-results-text')}
{/if}
</div>
</PopupComponent>
{/if}
{#if selectedResult}
<Root portal={layoutManager.layout.root} bind:open={detailsDialogOpen}>
<DialogPortal class="gv-z-[2000]">
<DialogContent>
<DialogTitle>{selectedResult.layer.label}</DialogTitle>
<DialogDescription>
<FeatureDetails
zoomOnOver={false}
showZoomButton={false}
feature={selectedResult.feature}
{fields}
/>
</DialogDescription>
<DialogClose />
</DialogContent>
</DialogPortal>
</Root>
{/if}

Aller plus loin