Skip to content

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.ts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Aller plus loin