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.tspackages/common/src/lib/widgets/light-identify/light-identify.config.tspackages/common/src/lib/widgets/light-identify/light-identify.model.tspackages/common/src/lib/widgets/light-identify/LightIdentify.svelte
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
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
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
<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}