Source Identify
Source Identify
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/identify/identify.declaration.tspackages/common/src/lib/widgets/identify/identify.config.tspackages/common/src/lib/widgets/identify/address/address.models.tspackages/common/src/lib/widgets/identify/address/Address.sveltepackages/common/src/lib/widgets/identify/coordinates/AdvancedCoordinates.sveltepackages/common/src/lib/widgets/identify/coordinates/Altitude.sveltepackages/common/src/lib/widgets/identify/coordinates/coordinates.config.tspackages/common/src/lib/widgets/identify/coordinates/coordinates.models.tspackages/common/src/lib/widgets/identify/coordinates/Coordinates.sveltepackages/common/src/lib/widgets/identify/coordinates/DisplayCoordinates.sveltepackages/common/src/lib/widgets/identify/identify-accordion-trigger.sveltepackages/common/src/lib/widgets/identify/identify.i18n.tspackages/common/src/lib/widgets/identify/identify.state.svelte.tspackages/common/src/lib/widgets/identify/Identify.sveltepackages/common/src/lib/widgets/identify/map-services-features/MapServicePanelFeatureList.sveltepackages/common/src/lib/widgets/identify/map-services-panel/map-services-panels.models.tspackages/common/src/lib/widgets/identify/map-services-panel/PanelFeatureList.sveltepackages/common/src/lib/widgets/identify/map-services-panel/PanelLayer.sveltepackages/common/src/lib/widgets/identify/map-services-panel/PanelMapService.sveltepackages/common/src/lib/widgets/identify/map-services-panel/PanelMapServiceList.sveltepackages/common/src/lib/widgets/identify/map-services-panel/PanelRoot.sveltepackages/common/src/lib/widgets/identify/map-services/FeatureDetails.sveltepackages/common/src/lib/widgets/identify/map-services/IdentifyErrors.sveltepackages/common/src/lib/widgets/identify/map-services/map-service.models.tspackages/common/src/lib/widgets/identify/map-services/MapServices.sveltepackages/common/src/lib/widgets/identify/map-services/tree/TreeMapServiceIdentifyResults.sveltepackages/common/src/lib/widgets/identify/map-services/tree/TreeSublayerMapServiceIdentifyResults.sveltepackages/common/src/lib/widgets/identify/parcel/parcel.config.tspackages/common/src/lib/widgets/identify/parcel/Parcel.svelte
packages/common/src/lib/widgets/identify/identify.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { type IdentifyConfig, identifyConfigSchema } from './identify.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./Identify.svelte').then((Identify) => widgetFactorySvelte(Identify)), schema: () => identifyConfigSchema,} satisfies WidgetDeclaration;
export type IdentifyProps = WidgetProps<IdentifyConfig>;packages/common/src/lib/widgets/identify/identify.config.ts
import { defineWidgetConfig, inToolbarSchemaFrom } from '$lib/api/managers/configuration';import { i18nSchemaFrom } from '$lib/api/managers/i18n';import { identifyAddressConfigSchema } from '$lib/widgets/identify/address/address.models';import { identifyCoordinatesConfigSchema } from '$lib/widgets/identify/coordinates/coordinates.config';import { identifyParcelConfigSchema } from '$lib/widgets/identify/parcel/parcel.config';import { identifyMapServicesConfigSchema } from '$lib/widgets/identify/map-services/map-service.models';import { z } from 'zod';import { identifyI18n } from '$lib/widgets/identify/identify.i18n';
export enum IdentifyTab { address = 'address', data = 'data', parcel = 'parcel', coordinates = 'coordinates',}
export const identifyConfigSchema = defineWidgetConfig({ i18n: i18nSchemaFrom(identifyI18n), title: { fr: 'Emplacement cliqué', nl: 'NL - Emplacement cliqué', }, icon: { lucide: 'MapPin', }, onActivate: { deactivate: { classes: ['Draw', 'MeasureDistance', 'MeasureSurface', 'AdvancedSearch', 'Export', 'Report'], }, }, inToolbar: inToolbarSchemaFrom({ type: 'button', }), config: z .object({ addressConfig: identifyAddressConfigSchema.optional().prefault({}), coordinatesConfig: identifyCoordinatesConfigSchema.optional().prefault({}), parcelConfig: identifyParcelConfigSchema.optional().prefault({}), mapServicesConfig: identifyMapServicesConfigSchema.optional().prefault({}), openedTabs: z.array(z.enum(IdentifyTab)).optional().default([IdentifyTab.address, IdentifyTab.data]), displayedTabs: z .array(z.enum(IdentifyTab)) .optional() .default([IdentifyTab.address, IdentifyTab.data, IdentifyTab.parcel, IdentifyTab.coordinates]), tolerance: z.number().optional().default(10), reactiveResultsOnTocUpdate: z.boolean().optional().default(true), zoomOnFeatureOnPageChange: z.boolean().optional().default(false), listenToClick: z.boolean().optional().default(false), }) .optional() .prefault({}),});
export type IdentifyConfig = z.infer<typeof identifyConfigSchema>;packages/common/src/lib/widgets/identify/address/address.models.ts
import { z } from 'zod';import type { I18nRegistry } from '$lib/api/managers/i18n';
export const addressTranslations = { 'exact-address': { fr: 'Adresse', nl: 'NL - Adresse', }, 'approximative-address': { fr: 'Adresse approximative', nl: 'NL - Adress approximative', }, 'no-address-found': { fr: "Pas d'adresse trouvée", nl: "NL - Pas d'adresse trouvée", }, 'click-on-map': { fr: "Cliquez sur la carte pour déterminer l'adresse la plus proche", nl: "NL - Cliquez sur la carte pour déterminer l'adresse la plus proche", },} satisfies I18nRegistry;
export const identifyAddressConfigSchema = z .object({ buffer: z.number().optional(), collapsible: z.boolean().default(true), }) .prefault({});
export type IdentifyAddressConfig = z.infer<typeof identifyAddressConfigSchema>;packages/common/src/lib/widgets/identify/address/Address.svelte
<script lang="ts"> import { getMapManager } from '$lib/api/map'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { queryState } from '$lib/api/utils'; import Loader from '$lib/components/common/Loader.svelte'; import type { IdentifyAddressConfig } from '$lib/widgets/identify/address/address.models'; import type { ApiPoint } from '$lib/api/geometry';
interface Props { clickedPoint?: ApiPoint; config: IdentifyAddressConfig; i18nData: I18nRegistry; }
let { clickedPoint, i18nData, config }: Props = $props();
const mapManager = getMapManager(); const geocode = mapManager.services.geocode; const i18n = getI18n(i18nData);
const addressQuery = $derived.by(() => { if (!clickedPoint) return null;
return queryState({ queryFn: ({ signal }) => geocode.findClosestAddress({ x: clickedPoint!.x, y: clickedPoint!.y, wkid: clickedPoint!.wkid, buffer: config.buffer, signal, }), disabled: () => !clickedPoint, queryKey: `IdentifyAddressFor-${clickedPoint!.x}-${clickedPoint!.y}`, }); });</script>
<div class="gv-flex gv-justify-start"> <div class="gv-ml-2"> {#if !addressQuery} {i18n('click-on-map')} {:else if addressQuery.loading} <div class="gv-h-6 gv-size-5 gv-ml-2"> <Loader /> </div> {:else if addressQuery.error} {i18n('no-address-found')} {:else if addressQuery.data} {#if addressQuery.data.found} <span class="gv-font-bold" >{addressQuery.data.precisionLevel === 'house' ? i18n('exact-address') : i18n('approximative-address')}</span >: <span data-test-id="Identify-Address-FullAddress">{addressQuery.data.bestResultFullAddress}</span> {:else} {i18n('no-address-found')} {/if} {/if} </div></div>packages/common/src/lib/widgets/identify/coordinates/AdvancedCoordinates.svelte
<script lang="ts"> import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '$lib/components/shadcn/ui/accordion'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import DisplayCoordinates from '$lib/widgets/identify/coordinates/DisplayCoordinates.svelte'; import { Projections } from '$lib/api/managers/projection'; import type { ApiPoint } from '$lib/api/geometry'; import type { IdentifyCoordinatesConfig } from '$lib/widgets/identify/coordinates/coordinates.config'; import { ADVANCED_COORDINATES_VALUE, getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte';
interface Props { clickedPoint?: ApiPoint; config: IdentifyCoordinatesConfig; i18nData: I18nRegistry; dataTestIdPrefix: string; numberOfDigit: number; }
let { clickedPoint, config, i18nData, dataTestIdPrefix, numberOfDigit }: Props = $props();
const i18n = getI18n(i18nData); const identifyContext = getIdentifyContext();</script>
<Accordion bind:value={identifyContext.advancedCoordinatesPanelValue} class="gv-mt-3 gv-m-0"> <AccordionItem value={ADVANCED_COORDINATES_VALUE} class="gv-m-0"> <AccordionContent> {#if config.showLambert72Coordinates} <div class="gv-mt-2"> <DisplayCoordinates {dataTestIdPrefix} unitLabel="m" projection={Projections.LAMBERT_72} {i18nData} {numberOfDigit} {clickedPoint} projectionLabel={i18n('lambert-72-label')} /> </div> {/if} {#if config.showLambert2008Coordinates} <div class="gv-mt-2"> <DisplayCoordinates {dataTestIdPrefix} unitLabel="m" projection={Projections.LAMBERT_2008} {i18nData} {numberOfDigit} {clickedPoint} projectionLabel={i18n('lambert-2008-label')} /> </div> {/if} </AccordionContent> <AccordionTrigger data-test-id="Identify-Coordinates-Advanced-Trigger" class="gv-w-max gv-border-none gv-flex-row-reverse gv-text-base gv-text-body gv-p-0 gv-text-primary" > <span class="gv-underline gv-decoration-body gv-underline-offset-4">{i18n('advanced-data')}</span> </AccordionTrigger> </AccordionItem></Accordion>packages/common/src/lib/widgets/identify/coordinates/Altitude.svelte
<script lang="ts"> import type { ApiPoint } from '$lib/api/geometry'; import { queryState } from '$lib/api/utils'; import { getMapManager } from '$lib/api/map'; import type { IdentifyCoordinatesConfig } from '$lib/widgets/identify/coordinates/coordinates.config'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import Loader from '$lib/components/common/Loader.svelte'; import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte';
interface Props { clickedPoint?: ApiPoint; config: IdentifyCoordinatesConfig; i18nData: I18nRegistry; dataTestIdPrefix: string; }
let { clickedPoint, config, i18nData, dataTestIdPrefix }: Props = $props();
const mapManager = getMapManager(); const altitude = mapManager.services.altitude; const i18n = getI18n(i18nData); const identifyContext = getIdentifyContext();
const groundLabel = $derived.by(() => (identifyContext.advancedCoordinatesShown ? `${i18n('ground')}:` : ''));
const mntQueryState = $derived.by(() => { if (!clickedPoint) return; return queryState({ queryFn: () => altitude.getGroundAltitude({ point: clickedPoint!, extent: mapManager.getMapExtent(), layerId: config.mntLayerId, altitudeServiceUrl: config.mntMapServiceUrl, attributeKey: config.mntAttributeKey, toFixed: 2, }), disabled: () => !clickedPoint, queryKey: `IdentifyMNTQueryFor-${clickedPoint?.x}-${clickedPoint?.y}`, }); });
const mnsQueryState = $derived.by(() => { if (!clickedPoint) return; return queryState({ queryFn: () => altitude.getSurfaceAltitude({ point: clickedPoint!, extent: mapManager.getMapExtent(), layerId: config.mnsLayerId, altitudeServiceUrl: config.mnsMapServiceUrl, attributeKey: config.mnsAttributeKey, toFixed: 2, }), disabled: () => !clickedPoint, queryKey: `IdentifyMNSQueryFor-${clickedPoint?.x}-${clickedPoint?.y}`, }); });</script>
<div class="gv-flex gv-mt-2"> <div class="gv-w-4/12 gv-font-bold"> {i18n('altitude')}: </div> <div class="gv-w-8/12"> <div> {#if !mntQueryState} {i18n('no-data')} {:else if mntQueryState.loading} <div class="gv-h-6 gv-size-4 gv-ml-2"> <Loader /> </div> {:else if mntQueryState.data} <span data-test-id={`${dataTestIdPrefix}-GroundAltitude`}>{groundLabel} {mntQueryState.data}</span> m {:else} {i18n('no-data')} {/if} </div> {#if identifyContext.advancedCoordinatesShown} <div> {#if !mnsQueryState} {i18n('no-data')} {:else if mnsQueryState.loading} <div class="gv-h-6 gv-size-4 gv-ml-2"> <Loader /> </div> {:else if mnsQueryState.data} <span data-test-id={`${dataTestIdPrefix}-SurfaceAltitude`}> {i18n('surface')} {mnsQueryState.data} </span> m {:else} {i18n('no-data')} {/if} </div> {/if} </div></div>packages/common/src/lib/widgets/identify/coordinates/coordinates.config.ts
import { z } from 'zod';import type { I18nRegistry } from '$lib/api/managers/i18n';
export const coordinatesTranslations = { 'wgs-84-label': { fr: 'Coordonnées GPS', nl: 'NL - Coordonnées GPS', }, 'wgs-84-longitude': { fr: 'Longitude', nl: 'NL - Longitude', }, 'wgs-84-latitude': { fr: 'Latitude', nl: 'NL - Latitude', }, 'lambert-72-label': { fr: 'Lambert Belge 1972', nl: 'NL - Lambert Belge 1972', }, 'lambert-2008-label': { fr: 'Lambert Belge 2008', nl: 'NL - Lambert Belge 2008', }, altitude: { fr: 'Altitude', nl: 'NL - Altitude', }, ground: { fr: 'Sol', nl: 'NL - Sol', }, surface: { fr: 'Surface', nl: 'NL - Surface', }, 'ground-altitude': { fr: 'Terrain', nl: 'NL - Terrain', }, 'surface-altitude': { fr: 'Altitude de surface', nl: 'NL - Altitude de surface', }, 'no-coordinates': { fr: 'Pas de coordonnées', nl: 'NL - Pas de coordonnées', }, 'view-on-google-map': { fr: 'Ouvrir dans Google Maps', nl: 'NL - Ouvrir dans Google Maps', }, 'generate-report-from-point': { fr: 'Générer un rapport depuis ce point', nl: 'NL - Générer un rapport depuis ce point', }, 'copy-coordinates': { fr: 'Copier les coordonnées', nl: 'NL - Copier les coordonnées', }, 'copy-coordinates-success': { fr: 'Coordonnées copiées dans le presse papier', nl: 'NL - Coordonnées copiées dans le presse papier', }, 'copy-coordinates-error': { fr: 'Coordonnées non copiées dans le presse papier', nl: 'NL - Coordonnées non copiées dans le presse papier', }, 'no-data': { fr: 'Pas de données', nl: 'NL - Pas de données', }, 'advanced-data': { fr: 'Données avancées', nl: 'NL - Données avancées', },} satisfies I18nRegistry;
export const identifyCoordinatesConfigSchema = z .object({ collapsible: z.boolean().default(true), showWgs84Coordinates: z.boolean().optional().default(true), showLambert72Coordinates: z.boolean().optional().default(true), showLambert2008Coordinates: z.boolean().optional().default(true), showGoogleMapsCoordinates: z.boolean().optional().default(true), showAdvancedPanel: z.boolean().optional().default(true), showAltitude: z.boolean().optional().default(true), showReportButton: z.boolean().optional().default(false), numberOfDigit: z.number().optional().default(0), mntMapServiceUrl: z .string() .optional() .default('https://geoservices.wallonie.be/arcgis/rest/services/RELIEF/WALLONIE_MNT_2021_2022/MapServer'), mntLayerId: z.number().optional().default(0), mntAttributeKey: z.string().optional().default('Stretch.Pixel Value'), mnsMapServiceUrl: z .string() .optional() .default('https://geoservices.wallonie.be/arcgis/rest/services/RELIEF/WALLONIE_MNS_2021_2022/MapServer'), mnsLayerId: z.number().optional().default(0), mnsAttributeKey: z.string().optional().default('Stretch.Pixel Value'), reportClosestAddressBuffer: z.number().default(10), reportIdentifyTolerance: z.number().default(10), reportTemplateName: z.string().default('REPORT_VERTICAL_PDF'), templateApiUrl: z.string().default('https://geoservices.test.wallonie.be/geoviewer-services/api/template'), }) .prefault({});
export type IdentifyCoordinatesConfig = z.infer<typeof identifyCoordinatesConfigSchema>;packages/common/src/lib/widgets/identify/coordinates/coordinates.models.ts
export type ResultFormat = { x: string; y: string; xLabel: string; yLabel: string;};packages/common/src/lib/widgets/identify/coordinates/Coordinates.svelte
<script lang="ts"> import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { Projections } from '$lib/api/managers/projection'; import type { IdentifyCoordinatesConfig } from '$lib/widgets/identify/coordinates/coordinates.config'; import type { ApiPoint } from '$lib/api/geometry'; import { Button } from '$lib/components/shadcn/ui/button'; import { ExternalLink, MapPin } from 'lucide-svelte'; import DisplayCoordinates from '$lib/widgets/identify/coordinates/DisplayCoordinates.svelte'; import { getMapManager } from '$lib/api/map'; import { queryState, reverse } from '$lib/api/utils'; import Loader from '$lib/components/common/Loader.svelte'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { LegendNode } from '$lib/api/utils/legend/legend.model.svelte'; import BlockingLoader from '$lib/components/blocking-loader/BlockingLoader.svelte'; import { type GenerateReportParams, SelectionType } from '$lib/api/tools'; import { getDefaultTemplateParams } from '$lib/api/tools/report/report.utils'; import AdvancedCoordinates from '$lib/widgets/identify/coordinates/AdvancedCoordinates.svelte'; import Altitude from '$lib/widgets/identify/coordinates/Altitude.svelte';
interface Props { clickedPoint?: ApiPoint; config: IdentifyCoordinatesConfig; i18nData: I18nRegistry; }
let { clickedPoint, config, i18nData }: Props = $props();
const mapManager = getMapManager(); const { highlight, report, featureFactory } = mapManager.tools; const i18n = getI18n(i18nData);
const legendNodes = $derived.by(() => reverse(mapManager.layerList.list) .filter((x) => x.toc.visible && x.visible) .map((service) => new LegendNode(service)), ); const { numberOfDigit } = config; const dataTestIdPrefix = 'Identify-Coordinates'; let loading = $state<boolean>(false);
async function onGenerateReportClicked() { if (!clickedPoint) { throw new GeoviewerError('No clicked point, should never happen as button is disabled if so'); } loading = true; const pointFeature = featureFactory.createPoint(clickedPoint); highlight.highlightFeature(pointFeature); const generateReportParams: GenerateReportParams = { identifyTolerance: config.reportIdentifyTolerance, closestAddressBuffer: config.reportClosestAddressBuffer, templateParams: getDefaultTemplateParams(config.reportTemplateName, config.templateApiUrl), selectionType: SelectionType.POINT, legendNodes, }; report .downloadReport(pointFeature!, generateReportParams) .catch((err) => { throw new GeoviewerError(i18n('common.export-error'), { cause: err }); }) .finally(() => { highlight.unhighlightFeature(pointFeature!); loading = false; }); }</script>
<div class="gv-w-full"> {#if config.showWgs84Coordinates} <div> <DisplayCoordinates {dataTestIdPrefix} projection={Projections.WGS_1984} xLabel={i18n('wgs-84-longitude')} yLabel={i18n('wgs-84-latitude')} toDms {i18nData} isGoogleMap {numberOfDigit} {clickedPoint} projectionLabel={i18n('wgs-84-label')} /> </div> {/if} {#if clickedPoint && config.showReportButton} <div class="gv-flex gv-justify-end gv-mt-2"> <Button data-test-id="Identify-Coordinates-GenerateReportButton" onclick={onGenerateReportClicked}> <ExternalLink class="gv-size-4" />{i18n('generate-report-from-point')}</Button > </div> {/if} {#if config.showAltitude} <Altitude {config} {i18nData} {clickedPoint} {dataTestIdPrefix} /> {/if} {#if config.showAdvancedPanel} <AdvancedCoordinates {clickedPoint} {config} {i18nData} {dataTestIdPrefix} {numberOfDigit} /> {/if}</div>
<BlockingLoader open={loading} />packages/common/src/lib/widgets/identify/coordinates/DisplayCoordinates.svelte
<script lang="ts"> import { Copy, ExternalLink } from 'lucide-svelte'; import type { ResultFormat } from '$lib/widgets/identify/coordinates/coordinates.models'; import { type CardinalLabels, formatDms, formatNumber } from '$lib/api/utils'; import { showToast } from '$lib/components/toast/toast.utils'; import { type Projection, Projections } from '$lib/api/managers/projection'; import type { ApiPoint } from '$lib/api/geometry'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { ApiSimpleTooltip } from '$lib/components/api-simple-tooltip'; import { getMapManager } from '$lib/api/map'; import { copyToClipboard } from '$lib/api/utils/clipboard.utils'; import { Button } from '$lib/components/shadcn/ui/button';
interface Props { i18nData: I18nRegistry; projection: Projection; clickedPoint?: ApiPoint; toDms?: boolean; numberOfDigit: number; xLabel?: string; yLabel?: string; projectionLabel: string; dataTestIdPrefix?: string; unitLabel?: string; isGoogleMap?: boolean; } let { i18nData, projectionLabel, projection, clickedPoint, toDms = false, numberOfDigit, xLabel, yLabel, dataTestIdPrefix = '', unitLabel = '', isGoogleMap = false, }: Props = $props();
const i18n = getI18n(i18nData); const mapManager = getMapManager(); const transformTool = mapManager.tools.transform; const isWgs84 = $derived.by(() => { return projection.wkid === Projections.WGS_1984.wkid; });
const cardinalLabels: CardinalLabels = $derived.by(() => { return { north: i18n('common.cardinal-north'), south: i18n('common.cardinal-south'), west: i18n('common.cardinal-west'), east: i18n('common.cardinal-east'), }; });
const result = $derived.by(() => { const inDesiredProjection = transformTo(projection.wkid); if (!inDesiredProjection) return; return { x: toDms ? formatDms(inDesiredProjection.x, 'X', cardinalLabels) : formatCoordinate(inDesiredProjection.x, numberOfDigit), y: toDms ? formatDms(inDesiredProjection.y, 'Y', cardinalLabels) : formatCoordinate(inDesiredProjection.y, numberOfDigit), xLabel: xLabel ?? 'X', yLabel: yLabel ?? 'Y', } satisfies ResultFormat; });
function transformTo(wkid: number) { if (!clickedPoint) return; return transformTool.transformPoint(clickedPoint, wkid); }
function formatCoordinate(coord: number, numberOfDigit: number): string { return numberOfDigit === 0 ? `${Math.round(coord)}` : formatNumber(coord, 0, numberOfDigit); }
function copyCoordinates() { const point = transformTo(projection.wkid); if (!point) return;
// In WGS84 we invert X and Y so that pasting the coordinates to Google Maps works const xCoord = isWgs84 ? point.y : point.x; const yCoord = isWgs84 ? point.x : point.y;
copyToClipboard(`${xCoord}, ${yCoord}`, { onSuccess: () => showToast({ level: 'success', message: i18n('copy-coordinates-success') }), onError: () => showToast({ level: 'error', message: i18n('copy-coordinates-error') }), }); }
function checkOnGoogleMap() { if (clickedPoint) { const toWgs84 = transformTool.transformPoint(clickedPoint, Projections.WGS_1984.wkid); const link = `https://www.google.com/maps/@${toWgs84.y},${toWgs84.x},17z`; window.open(link, '_blank', 'noopener, noreferrer'); } }</script>
<div class="gv-flex gv-group"> <div class="gv-w-4/12 gv-font-bold"> {projectionLabel}: </div> <div class="gv-w-8/12 gv-flex gv-justify-between hover:gv-bg-muted-600"> <div class="gv-w-10/12"> {#if result} <div> {result.xLabel}: <span data-test-id={`${dataTestIdPrefix}-${projection.wkid}-X`}>{result.x} {unitLabel}</span> </div> <div> {result.yLabel}: <span data-test-id={`${dataTestIdPrefix}-${projection.wkid}-Y`}>{result.y} {unitLabel}</span> </div> {#if isGoogleMap} <Button class="gv-p-0 gv-text-muted-foreground gv-font-normal gv-underline" onclick={checkOnGoogleMap} variant="link" > {i18n('view-on-google-map')}<ExternalLink class="gv-size-4" /></Button > {/if} {:else} {i18n('no-coordinates')} {/if} </div> <div class="gv-w-2/12 gv-flex gv-justify-end"> <ApiSimpleTooltip text={i18n('copy-coordinates')}> <button class="gv-opacity-0 group-hover:gv-opacity-100" onclick={copyCoordinates}> <Copy class="gv-h-4" /> </button> </ApiSimpleTooltip> </div> </div></div>packages/common/src/lib/widgets/identify/identify-accordion-trigger.svelte
<script lang="ts"> import { AccordionTrigger, type TriggerProps } from '$lib/components/shadcn/ui/accordion'; import type { PropsWithChildren } from '$lib/api/utils'; import { cn } from '$lib/components/shadcn/utils';
interface Props extends TriggerProps { collapsible: boolean; }
let { collapsible, children, class: className, ...restProps }: PropsWithChildren<Props> = $props();</script>
<AccordionTrigger {...restProps} class={cn('data-[state=closed]:gv-border-none', className)} disabled={!collapsible} chevron={collapsible}> <div class="gv-flex gv-items-center gv-justify-start"> {@render children?.()} </div></AccordionTrigger>packages/common/src/lib/widgets/identify/identify.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';import { coordinatesTranslations } from '$lib/widgets/identify/coordinates/coordinates.config';import { addressTranslations } from '$lib/widgets/identify/address/address.models';import { parcelTranslations } from '$lib/widgets/identify/parcel/parcel.config';import { mapServicesTranslations } from '$lib/widgets/identify/map-services/map-service.models';import { mapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models';
export const identifyI18n = { address: { fr: 'Adresse', nl: 'NL - Adresse', }, coordinates: { fr: 'Localisation', nl: 'NL - Localisation', }, 'parcel-reference': { fr: 'Référence cadastrale', nl: 'NL - Référence cadastrale', }, 'data-relative-info': { fr: 'Infos relatives aux données', nl: 'NL - Infos relatives aux données', }, ...coordinatesTranslations, ...addressTranslations, ...parcelTranslations, ...mapServicesTranslations, ...mapServicesPanelsI18n,} satisfies I18nRegistry;packages/common/src/lib/widgets/identify/identify.state.svelte.ts
import { getContext, setContext } from 'svelte';import type { IdentifyResultTreeQueryState } from '$lib/widgets/identify/map-services/map-service.models';import type { IdentifyLayerResultTree, IdentifyMapServiceResultTree } from '$lib/api/utils';
export type IdentifyMapServicesPanels = 'root' | 'mapservices-list' | 'mapservice' | 'sublayers' | 'results';
export const ADVANCED_COORDINATES_VALUE = 'advanced-coordinates';
export class IdentifyState { public currentPanel = $state<IdentifyMapServicesPanels>('root'); public identifyStates = $state<IdentifyResultTreeQueryState[]>([]); public atLeastOneMapServiceHasResults = $state<boolean>(false); public selectedIdentifyResult = $state<IdentifyLayerResultTree | IdentifyMapServiceResultTree | undefined>(); public selectionHistory = $state<(IdentifyMapServiceResultTree | IdentifyLayerResultTree)[]>([]); public advancedCoordinatesPanelValue = $state<string | undefined>(); public advancedCoordinatesShown = $derived.by( () => this.advancedCoordinatesPanelValue === ADVANCED_COORDINATES_VALUE, );
public moveForward(selection: IdentifyMapServiceResultTree | IdentifyLayerResultTree | undefined): void { if (!selection) { return; } this.selectedIdentifyResult = selection; this.currentPanel = selection.type === 'IdentifyMapServiceResultTree' ? 'mapservice' : 'sublayers'; this.selectionHistory.push(selection); }
public moveBackward(): void { this.selectionHistory.pop(); if (this.selectionHistory.length !== 0) { const previousStep = this.selectionHistory[this.selectionHistory.length - 1]; this.currentPanel = previousStep.type === 'IdentifyMapServiceResultTree' ? 'mapservice' : 'sublayers'; this.selectedIdentifyResult = previousStep; } if (this.selectionHistory.length === 0) { this.currentPanel = 'mapservices-list'; } }
public identifyLoading = $derived.by(() => { return this.identifyStates.some((x) => x.queryState.loading); });}
const IDENTIFY_CONTEXT_KEY = 'IDENTIFY_CONTEXT_KEY';
export function setIdentifyContext(context: IdentifyState) { setContext(IDENTIFY_CONTEXT_KEY, context); return getIdentifyContext();}
export function getIdentifyContext(): IdentifyState { const context = getContext<IdentifyState>(IDENTIFY_CONTEXT_KEY); if (!context) { throw new Error('Identify not found in context.'); } return context;}packages/common/src/lib/widgets/identify/Identify.svelte
<script lang="ts"> import type { ApiPoint } from '$lib/api/geometry'; 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 ApiMapService, isIdentifiable } from '$lib/api/mapservices'; import { GeoviewerIdentifyError, type IdentifyMapServiceResultTree, mapServiceIdentifyResultToIdentifyTree, queryState, } from '$lib/api/utils'; import { Accordion, AccordionContent, AccordionItem } from '$lib/components/shadcn/ui/accordion'; import Address from './address/Address.svelte'; import Coordinates from './coordinates/Coordinates.svelte'; import { IdentifyState, setIdentifyContext } from './identify.state.svelte'; import PanelLayer from './map-services-panel/PanelLayer.svelte'; import PanelMapService from './map-services-panel/PanelMapService.svelte'; import PanelMapServiceList from './map-services-panel/PanelMapServiceList.svelte'; import type { IdentifyResultTreeQueryState } from './map-services/map-service.models'; import MapServices from './map-services/MapServices.svelte'; import Parcel from './parcel/Parcel.svelte'; import { Layers, LocateFixed, Navigation } from 'lucide-svelte'; import { onDestroy, untrack } from 'svelte'; import type { Unsubscriber } from 'svelte/store'; import { IdentifyTab } from './identify.config'; import type { IdentifyProps } from './identify.declaration'; import IdentifyAccordionTrigger from './identify-accordion-trigger.svelte'; import { Icon } from '$lib/components/icon'; import { showToast } from '$lib/components/toast/toast.utils';
let { fullConfig }: IdentifyProps = $props();
const config = fullConfig.config; const i18n = getI18n(fullConfig.i18n); const mapManager = getMapManager(); const identifyState = setIdentifyContext(new IdentifyState());
let lastClickedPoint: ApiPoint | undefined; let clickedPoint = $state<ApiPoint | undefined>();
// Add unclosable tab as default to simplify config const defaultOpened = [...config.openedTabs]; if (!config.addressConfig.collapsible) { defaultOpened.push(IdentifyTab.address); } if (!config.coordinatesConfig.collapsible) { defaultOpened.push(IdentifyTab.coordinates); } if (!config.parcelConfig.collapsible) { defaultOpened.push(IdentifyTab.parcel); } if (!config.mapServicesConfig.collapsible) { defaultOpened.push(IdentifyTab.data); } let openedTabs = $state<string[]>(defaultOpened);
let onRightClickUnsubscribe: Unsubscriber | undefined;
if (config.listenToClick) { onRightClickUnsubscribe = mapManager.tools.events.on('rightClick', async (point) => { clickedPoint = point; }); }
$effect(() => { if (clickedPoint && (config.reactiveResultsOnTocUpdate || lastClickedPoint != clickedPoint)) { const areSamePoint = areSamePoints(clickedPoint, lastClickedPoint); if ( !areSamePoint && (identifyState.currentPanel === 'sublayers' || identifyState.currentPanel === 'mapservice') ) { identifyState.currentPanel = 'mapservices-list'; } lastClickedPoint = clickedPoint; identifyState.atLeastOneMapServiceHasResults = false; const identifiableLayers = untrack(() => mapManager.layerList.list.filter((l) => l.toc.visible && l.visible && isIdentifiable(l)).toReversed(), ); identifyState.identifyStates = identifiableLayers.map((layer) => getMapServiceIdentifyState(layer)); } });
export function updatePoint(newPoint: ApiPoint): void { clickedPoint = newPoint; }
function areSamePoints(a: ApiPoint | undefined, b: ApiPoint | undefined): boolean { if (!a || !b) { return false; } return a.x === b.x && a.y === b.y && a.wkid === b.wkid; }
function getMapServiceIdentifyState(mapService: ApiMapService): IdentifyResultTreeQueryState { return { mapService: mapService, queryState: queryState<IdentifyMapServiceResultTree, never>({ queryFn: () => identifyMapService(clickedPoint!, mapService), disabled: () => !clickedPoint, }), }; }
function identifyMapService( clickedPoint: ApiPoint, mapService: ApiMapService, ): Promise<IdentifyMapServiceResultTree> { if (!isIdentifiable(mapService)) { throw new GeoviewerError('Should never happen but Typescript is stubborn'); } return mapService .identify(clickedPoint, { returnGeometry: true, tolerance: config.tolerance, }) .then((res) => { if (res.totalResultsCount > 0) { identifyState.atLeastOneMapServiceHasResults = true; } return mapServiceIdentifyResultToIdentifyTree({ mapService: mapService, identifyResponse: res, }); }) .catch((err) => { const geoviewerIdentifyError = err instanceof GeoviewerIdentifyError; if (!geoviewerIdentifyError) { showError(err); } return { service: mapService, features: [], type: 'IdentifyMapServiceResultTree', children: [], resultsCount: 0, errors: geoviewerIdentifyError ? [err] : [], } satisfies IdentifyMapServiceResultTree; }); }
function showError(err: object) { if (err instanceof GeoviewerError) { showToast(err.format()); } }
function showTab(tab: IdentifyTab): boolean { return config.displayedTabs.indexOf(tab) > -1; }
onDestroy(() => { onRightClickUnsubscribe?.(); });</script>
{#if identifyState.currentPanel === 'root'} <Accordion bind:value={openedTabs} multiple={true}> {#if showTab(IdentifyTab.address)} <AccordionItem value={IdentifyTab.address} class="gv-px-5 gv-py-2.5 gv-m-0" disabled={!config.addressConfig.collapsible} > <IdentifyAccordionTrigger data-test-id="Identify-PanelTrigger-Address" collapsible={config.addressConfig.collapsible} > <Navigation class="gv-h-4 gv-text-primary" /> <span class="gv-font-bold">{i18n('address')}</span> </IdentifyAccordionTrigger> <AccordionContent class="gv-p-2"> <Address i18nData={fullConfig.i18n} config={config.addressConfig} {clickedPoint} /> </AccordionContent> </AccordionItem> <div class="gv-border-b gv-h-[1px]"></div> {/if} {#if showTab(IdentifyTab.coordinates)} <AccordionItem value={IdentifyTab.coordinates} class="gv-px-5 gv-py-2.5 gv-m-0" disabled={!config.coordinatesConfig.collapsible} > <IdentifyAccordionTrigger data-test-id="Identify-PanelTrigger-Coordinates" collapsible={config.coordinatesConfig.collapsible} > <LocateFixed class="gv-h-4 gv-text-primary" /> <span class="gv-font-bold">{i18n('coordinates')}</span> </IdentifyAccordionTrigger> <AccordionContent class="gvp-p-2"> <Coordinates i18nData={fullConfig.i18n} config={config.coordinatesConfig} {clickedPoint} /> </AccordionContent> </AccordionItem> <div class="gv-border-b gv-h-[1px]"></div> {/if} {#if showTab(IdentifyTab.parcel)} <AccordionItem value={IdentifyTab.parcel} class="gv-px-5 gv-py-2.5 gv-m-0" disabled={!config.parcelConfig.collapsible} > <IdentifyAccordionTrigger data-test-id="Identify-PanelTrigger-Parcel" collapsible={config.parcelConfig.collapsible} > <Icon class="gv-mx-1 gv-text-primary" icon={{ geoviewer: 'parcel' }} /> <span class="gv-font-bold">{i18n('parcel-reference')}</span> </IdentifyAccordionTrigger> <AccordionContent class="gv-p-2"> <Parcel i18nData={fullConfig.i18n} config={config.parcelConfig} {clickedPoint} /> </AccordionContent> </AccordionItem> <div class="gv-border-b gv-h-[1px]"></div> {/if} {#if showTab(IdentifyTab.data)} <AccordionItem value={IdentifyTab.data} class="gv-px-5 gv-py-2.5 gv-m-0" disabled={!config.mapServicesConfig.collapsible} > <IdentifyAccordionTrigger data-test-id="Identify-PanelTrigger-Data" collapsible={config.mapServicesConfig.collapsible} > <Layers class="gv-h-4 gv-text-primary" /> <span class="gv-font-bold">{i18n('data-relative-info')}</span> </IdentifyAccordionTrigger> <AccordionContent class="gv-max-h-full gv-overflow-y-auto"> <MapServices i18nData={fullConfig.i18n} config={config.mapServicesConfig} {clickedPoint} /> </AccordionContent> </AccordionItem> <div class="gv-border-b gv-h-[1px]"></div> {/if} </Accordion>{/if}{#if identifyState.currentPanel === 'mapservices-list'} <PanelMapServiceList i18nData={fullConfig.i18n} />{/if}{#if identifyState.currentPanel === 'mapservice' && identifyState.selectedIdentifyResult?.type === 'IdentifyMapServiceResultTree'} <PanelMapService />{/if}{#if identifyState.currentPanel === 'sublayers' && identifyState.selectedIdentifyResult?.type === 'IdentifyLayerResultTree'} <PanelLayer zoomOnFeatureOnPageChange={config.zoomOnFeatureOnPageChange} i18n={fullConfig.i18n} />{/if}packages/common/src/lib/widgets/identify/map-services-features/MapServicePanelFeatureList.svelte
<script lang="ts"> import type { ApiFeature } from '$lib/api/feature'; import FeatureDetails from '$lib/widgets/identify/map-services/FeatureDetails.svelte'; import Pagination from '$lib/components/pagination/Pagination.svelte'; import { getMapManager } from '$lib/api/map'; import { Button } from '$lib/components/shadcn/ui/button'; import { Expand } from 'lucide-svelte'; import { DialogClose, DialogContent, DialogDescription, DialogPortal, DialogTitle, Root, } from '$lib/components/shadcn/ui/dialog'; import { getI18n } from '$lib/api/managers/i18n'; import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import { getLayoutManager } from '$lib/api/managers/layout'; import { isGraphicService } from '$lib/api/utils'; import type { ApiMapService } from '$lib/api/mapservices';
interface Props { features: ApiFeature[]; mapService: ApiMapService; i18n: MapServicesPanelsI18n; zoomOnFeatureOnPageChange?: boolean; } let { features, mapService, i18n: i18nConfig, zoomOnFeatureOnPageChange = false }: Props = $props();
const mapManager = getMapManager(); const layoutManager = getLayoutManager(); const zoom = mapManager.tools.zoom; const i18n = getI18n(i18nConfig); const fields = $derived.by(() => (isGraphicService(mapService) ? mapService.fields : []));
let selectedIndex = $state<number>(0); let feature = $derived(features[selectedIndex]); let detailsDialogOpen = $state<boolean>(false);
$effect(() => { if (zoomOnFeatureOnPageChange && feature) { zoom.zoomToFeature(feature); } });</script>
<Pagination count={features.length} bind:page={selectedIndex} perPage={1} variant="ghost" size="sm" />
<div class="gv-flex gv-justify-center gv-text-sm gv-gap-1"> <span data-test-id="Pagination-TotalElements">{features.length}</span> {i18n('elements')}</div>
<FeatureDetails containerClass="gv-max-h-96 gv-mb-1" {feature} {fields} />
<Button onclick={() => (detailsDialogOpen = true)} variant="outline"> <Expand class="gv-size-4" /> {i18n('common.expand')}</Button>
<Root portal={layoutManager.layout.root} bind:open={detailsDialogOpen}> <DialogPortal class="gv-z-[2000]"> <DialogContent> <DialogTitle>{mapService.label}</DialogTitle> <DialogDescription> <FeatureDetails containerClass="gv-max-h-96" zoomOnOver={false} showZoomButton={false} {feature} {fields} /> </DialogDescription> <DialogClose /> </DialogContent> </DialogPortal></Root>packages/common/src/lib/widgets/identify/map-services-panel/map-services-panels.models.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const mapServicesPanelsI18n = { elements: { fr: 'élément(s)', nl: 'NL - élément(s)', }, NON_QUERYABLE_LAYER: { fr: 'Fonctionnalité non disponible pour cette donnée', nl: 'NL - Fonctionnalité non disponible pour cette donnée', }, NO_QUERYABLE_LAYER_FOUND: { fr: 'Aucune couche identifiable trouvée', nl: 'NL - Aucune couche identifiable trouvée', }, BAD_PARAMETERS: { fr: 'Paramètres de la requête invalide', nl: 'NL - Paramètres de la requête invalide', }, GEOJSON_NOT_SUPPORTED: { fr: 'Fonctionnalité non disponible pour cette donnée (GeoJSON)', nl: 'NL - Fonctionnalité non disponible pour cette donnée (GeoJSON)', }, WMS_SIDE_ERROR: { fr: 'Une erreur est survenue durant résolution de la requête', nl: 'NL - Une erreur est survenue durant résolution de la requête', }, SPATIAL_REFERENCE_NOT_SUPPORTED: { fr: "La référence spatiale actuelle n'est pas supportée par le service", nl: "NL - La référence spatiale actuelle n'est pas supportée par le service", },} satisfies I18nRegistry;
export type MapServicesPanelsI18n = typeof mapServicesPanelsI18n;packages/common/src/lib/widgets/identify/map-services-panel/PanelFeatureList.svelte
<script lang="ts"> import type { ApiFeature } from '$lib/api/feature'; import FeatureDetails from '$lib/widgets/identify/map-services/FeatureDetails.svelte'; import Pagination from '$lib/components/pagination/Pagination.svelte'; import { getMapManager } from '$lib/api/map'; import { Button } from '$lib/components/shadcn/ui/button'; import { Expand } from 'lucide-svelte'; import { DialogClose, DialogContent, DialogDescription, DialogPortal, DialogTitle, Root, } from '$lib/components/shadcn/ui/dialog'; import { getI18n } from '$lib/api/managers/i18n'; import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import { getLayoutManager } from '$lib/api/managers/layout'; import { isQueryable } from '$lib/api/utils'; import type { ApiSublayer } from '$lib/api/layers';
interface Props { features: ApiFeature[]; layer: ApiSublayer; i18n: MapServicesPanelsI18n; zoomOnFeatureOnPageChange?: boolean; } let { features, layer, i18n: i18nConfig, zoomOnFeatureOnPageChange = false }: Props = $props();
const mapManager = getMapManager(); const layoutManager = getLayoutManager(); const zoom = mapManager.tools.zoom; const i18n = getI18n(i18nConfig); const fields = $derived.by(() => (isQueryable(layer) ? layer.fields : []));
let selectedIndex = $state<number>(0); let feature = $derived(features[selectedIndex]); let detailsDialogOpen = $state<boolean>(false);
$effect(() => { if (zoomOnFeatureOnPageChange && feature) { zoom.zoomToFeature(feature); } });</script>
<Pagination count={features.length} bind:page={selectedIndex} perPage={1} variant="ghost" size="sm" />
<div class="gv-flex gv-justify-center gv-text-sm gv-gap-1"> <span data-test-id="Pagination-TotalElements">{features.length}</span> {i18n('elements')}</div>
<FeatureDetails containerClass="gv-max-h-96 gv-mb-1" {feature} {fields} />
<Button onclick={() => (detailsDialogOpen = true)} variant="outline"> <Expand class="gv-size-4" /> {i18n('common.expand')}</Button>
<Root portal={layoutManager.layout.root} bind:open={detailsDialogOpen}> <DialogPortal class="gv-z-[2000]"> <DialogContent> <DialogTitle>{layer.label}</DialogTitle> <DialogDescription> <FeatureDetails containerClass="gv-max-h-96" zoomOnOver={false} showZoomButton={false} {feature} {fields} /> </DialogDescription> <DialogClose /> </DialogContent> </DialogPortal></Root>packages/common/src/lib/widgets/identify/map-services-panel/PanelLayer.svelte
<script lang="ts"> import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte'; import { ChevronLeft, ChevronRight } from 'lucide-svelte'; import PanelFeatureList from '$lib/widgets/identify/map-services-panel/PanelFeatureList.svelte'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import { cn } from '$lib/components/shadcn/utils'; import { isQueryable } from '$lib/api/utils';
interface Props { zoomOnFeatureOnPageChange: boolean; i18n: MapServicesPanelsI18n; }
let { zoomOnFeatureOnPageChange, i18n: i18nConfig }: Props = $props();
const identifyContext = getIdentifyContext(); let selectedLayerResult = $derived.by(() => { if (identifyContext.selectedIdentifyResult?.type != 'IdentifyLayerResultTree') { throw new GeoviewerError('Current result is not of type IdentifyLayerResultTree'); } return identifyContext.selectedIdentifyResult; });</script>
{#if selectedLayerResult} <div class="gv-p-2"> <button data-test-id="Identify-Data-BackButton" class="gv-flex gv-mb-4 gv-mt-2 gv-font-bold" onclick={() => identifyContext.moveBackward()} > <ChevronLeft class="gv-pb-1" /> <span>{selectedLayerResult.layer.label}</span> </button> <ul class="gv-space-y-2 gv-p-2"> {#if !selectedLayerResult.children || selectedLayerResult.children.length === 0} {#if selectedLayerResult.features} <PanelFeatureList {zoomOnFeatureOnPageChange} layer={selectedLayerResult.layer} features={selectedLayerResult.features} i18n={i18nConfig} /> {/if} {:else} {#each selectedLayerResult.children as sublayerResult (sublayerResult.layer.id)} <button disabled={sublayerResult.featureCount === 0} data-test-id="Identify-Data-Sublayer-{sublayerResult.layer.label}" class={cn( 'gv-flex gv-w-full gv-items-center gv-border-b gv-border-grey-300 gv-cursor-pointer gv-pb-1 gv-transition-all gv-duration-200 hover:gv-border-primary', sublayerResult.featureCount === 0 && 'gv-opacity-50', )} onclick={() => identifyContext.moveForward(sublayerResult)} > <span class="gv-flex-grow gv-text-left"> {sublayerResult.layer.label} ({sublayerResult.featureCount ?? 0}) </span> <span> <ChevronRight /> </span> </button> {/each} {/if} </ul> </div>{/if}packages/common/src/lib/widgets/identify/map-services-panel/PanelMapService.svelte
<script lang="ts"> import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte'; import { ChevronLeft, ChevronRight } from 'lucide-svelte'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import { cn } from '$lib/components/shadcn/utils';
const identifyContext = getIdentifyContext(); let selectedMapServiceResult = $derived.by(() => { if (identifyContext.selectedIdentifyResult?.type != 'IdentifyMapServiceResultTree') { throw new GeoviewerError('Current result is not of type IdentifyMapServiceResultTree'); } return identifyContext.selectedIdentifyResult; });</script>
{#if selectedMapServiceResult} <div class="gv-p-2"> <button data-test-id="Identify-Data-BackButton" class="gv-flex gv-mb-4 gv-mt-2 gv-font-bold" onclick={() => identifyContext.moveBackward()} > <ChevronLeft class="gv-pb-1" /> <span>{selectedMapServiceResult.service.label}</span> </button> <ul class="gv-space-y-2 gv-p-2"> {#each selectedMapServiceResult.children as sublayerResult (sublayerResult.layer.id)} <button disabled={sublayerResult.featureCount === 0} data-test-id="Identify-Data-Sublayer-{sublayerResult.layer.label}" class={cn( 'gv-flex gv-w-full gv-items-center gv-border-b gv-border-grey-300 gv-cursor-pointer gv-pb-1 gv-transition-all gv-duration-200 hover:gv-border-primary', sublayerResult.featureCount === 0 && 'gv-opacity-50', )} onclick={() => identifyContext.moveForward(sublayerResult)} > <span class="gv-flex-grow gv-text-left"> {sublayerResult.layer.label} ({sublayerResult.featureCount ?? 0}) </span> <span> <ChevronRight /> </span> </button> {/each} </ul> </div>{/if}packages/common/src/lib/widgets/identify/map-services-panel/PanelMapServiceList.svelte
<script lang="ts"> import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte'; import { ChevronLeft, ChevronRight } from 'lucide-svelte'; import Loader from '$lib/components/common/Loader.svelte'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n';
interface Props { i18nData: I18nRegistry; }
let { i18nData }: Props = $props(); const i18n = getI18n(i18nData); const identifyContext = getIdentifyContext();
function onBackClicked() { identifyContext.currentPanel = 'root'; }</script>
<div class="gv-p-2"> <button data-test-id="Identify-Data-BackButton" class="gv-flex gv-mb-4 gv-mt-2 gv-font-bold" onclick={onBackClicked} > <ChevronLeft class="gv-pb-1" /> <span>{i18n('data-relative-info')}</span> </button>
{#if identifyContext.identifyStates.some((x) => x.queryState.loading)} <div class="gv-flex gv-w-full gv-justify-center gv-p-2"> <Loader /> </div> {/if}
<ul class="gv-space-y-2 gv-p-2"> {#each identifyContext.identifyStates as identifiableLayer (identifiableLayer.mapService.id)} {#if identifiableLayer.queryState.data && identifiableLayer.queryState.data.resultsCount > 0} <button data-test-id="Identify-Data-MapService-{identifiableLayer.queryState.data.service.label}" class="gv-flex gv-w-full gv-items-center gv-border-b gv-border-grey-300 gv-cursor-pointer gv-pb-1 gv-transition-all gv-duration-200 hover:gv-border-primary" onclick={() => identifyContext.moveForward(identifiableLayer.queryState.data)} > <span class="gv-flex-grow gv-text-left"> {identifiableLayer.queryState.data.service.label} ({identifiableLayer.queryState.data .resultsCount}) </span> <span> <ChevronRight /> </span> </button> {/if} {/each} </ul></div>packages/common/src/lib/widgets/identify/map-services-panel/PanelRoot.svelte
<script lang="ts"> import { Button } from '$lib/components/shadcn/ui/button'; import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte'; import Loader from '$lib/components/common/Loader.svelte'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n';
interface Props { i18nData: I18nRegistry; }
let { i18nData }: Props = $props();
const i18n = getI18n(i18nData); const identifyContext = getIdentifyContext();
function showMapServicesResults() { identifyContext.currentPanel = 'mapservices-list'; }</script>
<div class="gv-w-full">{i18n('explanation-text')}</div><div class="gv-w-full gv-mt-3 gv-flex gv-justify-end gv-align-middle"> <div class="gv-flex gv-h-6 gv-mt-2 gv-mr-2"> {#if identifyContext.identifyLoading} <Loader /> {/if} </div> <Button data-test-id="Identify-Data-ShowDataButton" disabled={!identifyContext.atLeastOneMapServiceHasResults} onclick={showMapServicesResults} > {i18n('show-data')} </Button></div>packages/common/src/lib/widgets/identify/map-services/FeatureDetails.svelte
<script lang="ts"> import type { ApiFeature } from '$lib/api/feature'; import { getMapManager } from '$lib/api/map'; import Search from 'lucide-svelte/icons/search'; import type { ApiFieldDescription } from '$lib/api/domain'; import { cn } from '$lib/components/shadcn/utils'; import { recordToCssStyle, type StyleRecord } from '$lib/api/utils'; import DisplayAttribute from '$lib/components/display-attribute/DisplayAttribute.svelte';
interface Props { feature: ApiFeature; fields: ApiFieldDescription[]; showZoomButton?: boolean; zoomOnOver?: boolean; containerClass?: string; containerStyleRecord?: StyleRecord; }
let { feature, fields, showZoomButton = false, zoomOnOver = true, containerClass, containerStyleRecord, }: Props = $props();
const mapManager = getMapManager(); const highlight = mapManager.tools.highlight; const zoom = mapManager.tools.zoom;
let attributes = $derived(feature && feature.attributes ? feature.attributes : {});
const style = $derived.by(() => recordToCssStyle(containerStyleRecord));</script>
<div role="region" aria-label="Attributes of feature" onmouseleave={() => zoomOnOver && highlight.unhighlightFeature(feature)} onmouseenter={() => zoomOnOver && highlight.highlightFeature(feature)} {style} class={cn('gv-overflow-y-auto gv-overflow-x-hidden gv-border-grey-400/50 gv-border-2 gv-p-2', containerClass)}> {#each Object.keys(attributes) as key (key)} {@const field = fields.find((x) => x.key === key)} <div class="gv-flex gv-w-full gv-mt-1"> <div data-test-id="Identify-Data-Label-{fields.find((x) => x.key === key)?.label ?? key}" class="gv-w-2/5 gv-truncate gv-font-bold gv-text-xs" > {fields.find((x) => x.key === key)?.label ?? key}: </div> <div data-test-id="Identify-Data-Value-{field?.label ?? field?.key}" class="gv-w-3/5 gv-text-xs gv-overflow-clip gv-text-ellipsis" > <DisplayAttribute {field} value={attributes[key]} /> </div> </div> {/each} {#if showZoomButton} <div class="gv-flex gv-w-full gv-justify-end"> <button onclick={() => zoom.zoomToFeature(feature)}> <Search /> </button> </div> {/if}</div>packages/common/src/lib/widgets/identify/map-services/IdentifyErrors.svelte
<script lang="ts"> import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import { getI18n } from '$lib/api/managers/i18n'; import Error from '../../../components/common/Error.svelte'; import type { GeoviewerIdentifyError } from '$lib/api/utils';
interface Props { errors: GeoviewerIdentifyError[]; i18nData: MapServicesPanelsI18n; }
let { errors, i18nData }: Props = $props();
const i18n = getI18n(i18nData);</script>
<div> {#each errors as error (error)} <Error message={i18n(error.errorCode)} /> {/each}</div>packages/common/src/lib/widgets/identify/map-services/map-service.models.ts
import { z } from 'zod';import type { ApiMapService, IdentifyResponse } from '$lib/api/mapservices';import type { QueryState, IdentifyMapServiceResultTree } from '$lib/api/utils';import type { I18nRegistry } from '$lib/api/managers/i18n';
export const mapServicesTranslations = { 'explanation-text': { fr: 'Pour obtenir les informations attributaires des données dessinées sur la carte cliquez ci-dessous', nl: 'NL - Pour obtenir les informations attributaires des données dessinées sur la carte cliquez ci-dessous', }, 'show-data': { fr: 'Afficher les infos', nl: 'NL - Afficher les infos', }, 'no-data-in-toc': { fr: "Pas de donnée à afficher car pas de donnée ajoutée sur la carte. Veuillez d'abord ajouter des données sur la carte.", nl: "NL - Pas de donnée à afficher car pas de donnée ajoutée sur la carte. Veuillez d'abord ajouter des données sur la carte.", }, 'no-data': { fr: "Aucune donnée relative à l'emplacement cliqué n'a été trouvée.", nl: "NL - Aucune donnée relative à l'emplacement cliqué n'a été trouvée.", }, 'add-data': { fr: 'Ajouter des données', nl: 'NL - Ajouter des données', },} satisfies I18nRegistry;
export const identifyMapServicesConfigSchema = z .object({ displayMode: z.enum(['tree', 'panel']).optional().default('tree'), hideWithoutResultLayer: z.boolean().default(false), addDataWidgetId: z.string().default('addDataWidgetId'), collapsible: z.boolean().default(true), }) .prefault({});
export type IdentifyMapServiceConfig = z.infer<typeof identifyMapServicesConfigSchema>;
export interface MapServiceIdentifyResult { mapService: ApiMapService; identifyResponse: IdentifyResponse;}
export interface IdentifyResultTreeQueryState { queryState: QueryState<IdentifyMapServiceResultTree, never>; mapService: ApiMapService;}packages/common/src/lib/widgets/identify/map-services/MapServices.svelte
<script lang="ts"> import { type IdentifyMapServiceConfig } from '$lib/widgets/identify/map-services/map-service.models'; import TreeMapServiceIdentifyResults from '$lib/widgets/identify/map-services/tree/TreeMapServiceIdentifyResults.svelte'; import PanelRoot from '$lib/widgets/identify/map-services-panel/PanelRoot.svelte'; import { getIdentifyContext } from '$lib/widgets/identify/identify.state.svelte'; import type { ApiPoint } from '$lib/api/geometry'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { Button } from '$lib/components/shadcn/ui/button'; import { Icon } from '$lib/components/icon'; import { getWidgetManager } from '$lib/api/managers/widget'; import { getMapManager } from '$lib/api/map';
interface Props { clickedPoint?: ApiPoint; config: IdentifyMapServiceConfig; i18nData: I18nRegistry; }
let { clickedPoint, i18nData, config }: Props = $props();
const identifyState = getIdentifyContext();
const mapManager = getMapManager(); const widgetManager = getWidgetManager(); const i18n = getI18n(i18nData);
const tocIsEmpty = $derived.by(() => mapManager.layerList.list.filter((x) => x.toc.visible).length === 0);
const allIdentifyResultAreEmpty = $derived.by(() => { return ( identifyState.identifyStates.every( (state) => !state.queryState.loading && state.queryState.data && state.queryState.data.resultsCount === 0, ) && !identifyState.identifyStates.some( (state) => !state.queryState.loading && state.queryState.data && state.queryState.data.errors && state.queryState.data.errors.length > 0, ) ); });
export function onAddData() { widgetManager.getReference(config.addDataWidgetId).activate(); }</script>
{#if clickedPoint && config.displayMode === 'tree'} <div class="gv-ml-2"> {#if tocIsEmpty} <div>{i18n('no-data-in-toc')}</div> <Button class="gv-mt-1" onclick={onAddData} size="sm"> <Icon class="gv-size-4" icon={{ geoviewer: 'add-layer' }} /> {i18n('add-data')} </Button> {/if} {#if !tocIsEmpty && allIdentifyResultAreEmpty} {i18n('no-data')} {/if} {#each identifyState.identifyStates as identifiableLayer (identifiableLayer.mapService.id)} {#if identifiableLayer.queryState.loading || (identifiableLayer.queryState.data && (identifiableLayer.queryState.data.resultsCount > 0 || !!identifiableLayer.queryState.data.errors?.length))} <TreeMapServiceIdentifyResults {config} {i18nData} identifyQueryState={identifiableLayer} /> {/if} {/each} </div>{:else if config.displayMode === 'panel'} <PanelRoot {i18nData} />{/if}packages/common/src/lib/widgets/identify/map-services/tree/TreeMapServiceIdentifyResults.svelte
<script lang="ts"> import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '$lib/components/shadcn/ui/accordion'; import type { IdentifyMapServiceConfig, IdentifyResultTreeQueryState, } from '$lib/widgets/identify/map-services/map-service.models'; import TreeSublayerMapServiceIdentifyResults from '$lib/widgets/identify/map-services/tree/TreeSublayerMapServiceIdentifyResults.svelte'; import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import { type IdentifyLayerResultTree, isGraphicService } from '$lib/api/utils'; import { getI18n } from '$lib/api/managers/i18n'; import MapServicePanelFeatureList from '$lib/widgets/identify/map-services-features/MapServicePanelFeatureList.svelte'; import IdentifyErrors from '$lib/widgets/identify/map-services/IdentifyErrors.svelte'; import TriangleAlert from 'lucide-svelte/icons/triangle-alert';
interface Props { identifyQueryState: IdentifyResultTreeQueryState; config: IdentifyMapServiceConfig; i18nData: MapServicesPanelsI18n; }
let { identifyQueryState, i18nData, config }: Props = $props(); const { mapService, queryState } = identifyQueryState; const i18n = getI18n();
const inError = $derived.by( () => queryState.loading || (queryState.data && queryState.data.errors && queryState.data.errors.length > 0), );
function displayLayerResult(layerResult: IdentifyLayerResultTree): boolean { if (!config.hideWithoutResultLayer) return true; return !!layerResult.featureCount && layerResult.featureCount > 0; }</script>
<Accordion multiple={true}> <AccordionItem value={mapService.id}> <AccordionTrigger disabled={queryState.loading}> <div class="gv-flex gv-gap-1 gv-pl-2 gv-font-bold"> {mapService.label} {queryState.data && !inError ? `(${queryState.data.resultsCount})` : ''} {#if inError} <TriangleAlert class="gv-size-4" /> {/if} {queryState.loading ? `${i18n('common.loading')} ...` : ''} </div> </AccordionTrigger> {#if !queryState.loading && queryState.data} <AccordionContent> {#if isGraphicService(mapService) && queryState.data.features} <div class="gv-pt-2"> <MapServicePanelFeatureList features={queryState.data.features} {mapService} i18n={i18nData} /> </div> {/if} {#if queryState.data.errors} <IdentifyErrors {i18nData} errors={queryState.data.errors} /> {:else} {#each queryState.data.children as sublayer (sublayer.layer.id)} {#if displayLayerResult(sublayer)} <div class="gv-ml-4"> <TreeSublayerMapServiceIdentifyResults {config} {i18nData} identifyLayerResultTree={sublayer} /> </div> {/if} {/each} {/if} </AccordionContent> {/if} </AccordionItem></Accordion>packages/common/src/lib/widgets/identify/map-services/tree/TreeSublayerMapServiceIdentifyResults.svelte
<script lang="ts"> import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '$lib/components/shadcn/ui/accordion'; import TreeSublayerMapServiceIdentifyResults from '$lib/widgets/identify/map-services/tree/TreeSublayerMapServiceIdentifyResults.svelte'; import { cn } from '$lib/components/shadcn/utils'; import PanelFeatureList from '$lib/widgets/identify/map-services-panel/PanelFeatureList.svelte'; import type { MapServicesPanelsI18n } from '$lib/widgets/identify/map-services-panel/map-services-panels.models'; import type { IdentifyMapServiceConfig } from '$lib/widgets/identify/map-services/map-service.models'; import { type IdentifyLayerResultTree } from '$lib/api/utils'; import IdentifyErrors from '$lib/widgets/identify/map-services/IdentifyErrors.svelte'; import TriangleAlert from 'lucide-svelte/icons/triangle-alert';
interface Props { identifyLayerResultTree: IdentifyLayerResultTree; i18nData: MapServicesPanelsI18n; config: IdentifyMapServiceConfig; }
let { identifyLayerResultTree, i18nData, config }: Props = $props(); const { layer, children, features, featureCount, errors } = identifyLayerResultTree;
const inError = $derived.by(() => errors && errors.length > 0);
const disabled = $derived.by(() => { return (!children || children.length === 0) && (!features || features.length === 0) && !inError; });
function displayLayerResult(layerResult: IdentifyLayerResultTree): boolean { if (!config.hideWithoutResultLayer) return true; return !!layerResult.featureCount && layerResult.featureCount > 0; }</script>
<Accordion multiple={true}> <AccordionItem value={`${layer.id}`}> <AccordionTrigger {disabled} class={cn( 'gv-pl-2 gv-text-left gv-flex gv-justify-between gv-items-start', (!featureCount || featureCount === 0) && 'gv-opacity-50', )} > <div class="gv-flex gv-gap-1 gv-font-bold"> {layer.label} ({featureCount ?? 0}) {#if inError} <TriangleAlert class="gv-size-4" /> {/if} </div> </AccordionTrigger>
<AccordionContent class="gv-ml-4 gv-h-full gv-overflow-y-auto"> {#if features && features.length > 0} <PanelFeatureList i18n={i18nData} {layer} {features} /> {:else if errors && errors.length > 0} <IdentifyErrors {errors} {i18nData} /> {:else if children && children.length > 0} {#each children as child (child.layer.id)} {#if displayLayerResult(child)} <TreeSublayerMapServiceIdentifyResults {config} {i18nData} identifyLayerResultTree={child} /> {/if} {/each} {/if} </AccordionContent> </AccordionItem></Accordion>packages/common/src/lib/widgets/identify/parcel/parcel.config.ts
import { z } from 'zod';import type { I18nRegistry } from '$lib/api/managers/i18n';
export const parcelTranslations = { capakey: { fr: 'Capakey', nl: 'NL - Capakey', }, commune: { fr: 'Commune/INS', nl: 'NL - Commune/INS', }, 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', }, 'not-found': { fr: 'Pas de parcelle trouvée', nl: 'NL - Pas de parcelle trouvée', }, 'copy-capakey': { fr: 'Copier la capakey', nl: 'NL - Copier la capakey', }, 'copy-capakey-success': { fr: 'Capakey copiée dans le presse papier', nl: 'NL - Capakey copiée dans le presse papier', }, 'copy-capakey-error': { fr: 'Capakey non copiée dans le presse papier', nl: 'NL - Capakey non copiée dans le presse papier', }, 'generate-report-from-parcel': { fr: 'Générer un rapport depuis cette parcelle', nl: 'NL - Générer un rapport depuis cette parcelle', }, zoom: { fr: 'Zoomer sur la parcelle', nl: 'NL - Zoomer sur la parcelle', },} satisfies I18nRegistry;
export const identifyParcelConfigSchema = z .object({ collapsible: z.boolean().default(true), reportClosestAddressBuffer: z.number().default(10), reportIdentifyTolerance: z.number().default(10), showReportButton: z.boolean().optional().default(false), reportTemplateName: z.string().default('REPORT_VERTICAL_PDF'), templateApiUrl: z.string().default('https://geoservices.test.wallonie.be/geoviewer-services/api/template'), }) .prefault({});
export type IdentifyParcelConfig = z.infer<typeof identifyParcelConfigSchema>;packages/common/src/lib/widgets/identify/parcel/Parcel.svelte
<script lang="ts"> import { queryState, reverse } from '$lib/api/utils'; import { getI18n, type I18nRegistry } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import { Button } from '$lib/components/shadcn/ui/button'; import { type GenerateReportParams, SelectionType } from '$lib/api/tools'; import { showToast } from '$lib/components/toast/toast.utils'; import Loader from '$lib/components/common/Loader.svelte'; import { Copy, ExternalLink } from 'lucide-svelte'; import type { IdentifyParcelConfig } from '$lib/widgets/identify/parcel/parcel.config'; import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model'; import BlockingLoader from '$lib/components/blocking-loader/BlockingLoader.svelte'; import { LegendNode } from '$lib/api/utils/legend/legend.model.svelte'; import type { ApiPoint } from '$lib/api/geometry'; import { getDefaultTemplateParams } from '$lib/api/tools/report/report.utils'; import { copyToClipboard } from '$lib/api/utils/clipboard.utils'; import type { ParcelResult } from '$lib/api/services'; import { ApiSimpleTooltip } from '$lib/components/api-simple-tooltip';
interface Props { clickedPoint?: ApiPoint; config: IdentifyParcelConfig; i18nData: I18nRegistry; }
let { clickedPoint, i18nData, config }: Props = $props();
const mapManager = getMapManager(); const parcel = mapManager.services.parcel; const report = mapManager.tools.report; const highlight = mapManager.tools.highlight; const i18n = getI18n(i18nData); const legendNodes = $derived.by(() => reverse(mapManager.layerList.list) .filter((x) => x.toc.visible && x.visible) .map((service) => new LegendNode(service)), );
let loading = $state<boolean>(false);
const parcelQuery = $derived.by(() => { if (!clickedPoint) return;
return queryState({ queryFn: () => parcel.getParcelShapeByPoint(clickedPoint!), disabled: () => !clickedPoint, queryKey: `IdentifyParcelQueryFor-${clickedPoint?.x}-${clickedPoint?.y}`, }); });
function copyCapakey(result: ParcelResult | undefined) { if (!result) { return; } copyToClipboard(result.capakey, { onSuccess: () => showToast({ level: 'success', message: i18n('copy-capakey-success') }), onError: () => showToast({ level: 'error', message: i18n('copy-capakey-error') }), }); }
async function onGenerateReportClicked() { if (!parcelQuery?.data) { throw new GeoviewerError('No parcel, should never happen as button is disabled if so'); } loading = true; highlightParcel(parcelQuery.data); const generateReportParams: GenerateReportParams = { identifyTolerance: config.reportIdentifyTolerance, closestAddressBuffer: config.reportClosestAddressBuffer, templateParams: getDefaultTemplateParams(config.reportTemplateName, config.templateApiUrl), selectionType: SelectionType.POINT, legendNodes, }; report .downloadReport(parcelQuery.data.feature, generateReportParams) .catch((err) => { throw new GeoviewerError(i18n('common.export-error'), { cause: err }); }) .finally(() => { unhighlightParcel(parcelQuery.data); loading = false; }); }
function unhighlightParcel(result: ParcelResult | undefined) { if (!result) { return; } highlight.unhighlightFeature(result.feature); }
function highlightParcel(result: ParcelResult | undefined) { if (!result) { return; } highlight.highlightFeature(result.feature); }</script>
<div class="gv-w-full gv-ml-2"> {#if !parcelQuery} {i18n('not-found')} {:else if parcelQuery.loading} <div class="gv-h-6 gv-size-5 gv-ml-2"> <Loader /> </div> {:else if parcelQuery.data} <div role="region" aria-label="Description of the element" onmouseleave={() => unhighlightParcel(parcelQuery.data)} onmouseenter={() => highlightParcel(parcelQuery.data)} > <div class="gv-flex gv-mt-1 gv-group hover:gv-bg-muted-600"> <div class="gv-w-1/2 gv-font-bold"> {i18n('capakey')}: </div> <div class="gv-w-1/2 gv-flex gv-justify-between"> <div data-test-id="Identify-Parcel-capakey">{parcelQuery.data.capakey}</div> <div class="gv-flex gv-justify-end"> <ApiSimpleTooltip text={i18n('copy-coordinates')}> <button class="gv-text-primary"> <Copy onclick={() => copyCapakey(parcelQuery.data)} class="gv-h-4" /> </button> </ApiSimpleTooltip> </div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('commune')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-commune"> {parcelQuery.data.nomCommune} {parcelQuery.data.ins} </div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('division')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-division">{parcelQuery.data.division}</div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('section')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-section">{parcelQuery.data.section}</div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('radical')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-radical">{parcelQuery.data.radical}</div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('exposant')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-exposant">{parcelQuery.data.exposant}</div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('puissance')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-puissance">{parcelQuery.data.puissance}</div> </div> </div> <div class="gv-flex gv-mt-1"> <div class="gv-w-1/2 gv-font-bold"> {i18n('bis')}: </div> <div class="gv-w-1/2"> <div data-test-id="Identify-Parcel-bis">{parcelQuery.data.bis}</div> </div> </div> </div> {#if config.showReportButton} <div class="gv-flex gv-justify-end gv-mt-2"> <Button data-test-id="Identify-Parcel-GenerateReportButton" onclick={onGenerateReportClicked}> <ExternalLink class="gv-size-4" />{i18n('generate-report-from-parcel')}</Button > </div> {/if} {:else} {i18n('not-found')} {/if}</div>
<BlockingLoader open={loading} />