Source MeasureElevationProfile
Source MeasureElevationProfile
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/measure/elevation-profile/measure-elevation-profile.declaration.tspackages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.config.tspackages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.i18n.tspackages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.model.tspackages/common/src/lib/widgets/measure/elevation-profile/MeasureElevationProfile.sveltepackages/common/src/lib/widgets/measure/elevation-profile/MeasureElevationProfileChart.svelte
packages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { measureElevationProfileConfigSchema, type MeasureElevationProfileWidgetConfig,} from './measure-elevation-profile.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = { factory: () => import('./MeasureElevationProfile.svelte').then((MeasureElevationProfile) => widgetFactorySvelte(MeasureElevationProfile), ), schema: () => measureElevationProfileConfigSchema,} satisfies WidgetDeclaration;
export type MeasureElevationProfileProps = WidgetProps<MeasureElevationProfileWidgetConfig>;packages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration';import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n';import { measureI18n } from '$lib/widgets/measure/measure.i18n';import { z } from 'zod';import { measureElevationProfileI18n } from '$lib/widgets/measure/elevation-profile/measure-elevation-profile.i18n';import { dialogWidgetConfigSchema } from '$lib/components/containers/dialog/dialog.schema';import { colorSchema, defaultHighlightSymbolColor, pointSymbolSchema, polylineSymbolSchema } from '$lib/api/symbol';
export const measureElevationProfileConfigSchema = defineWidgetConfig({ icon: { lucide: 'ChartLine', }, title: measureElevationProfileI18n['tab-title'], onActivate: { deactivate: { classes: ['MeasureSurface', 'AdvancedSearch', 'Report', 'Export', 'Identify', 'AddData', 'Draw'], }, }, inToolbar: inToolbarSchemaFrom({ type: 'button', }), i18n: i18nSchemaFrom({ ...measureI18n, ...measureElevationProfileI18n }), config: z .object({ polylineDrawSymbol: polylineSymbolSchema, pointHoverSymbol: pointSymbolSchema.prefault({ type: 'simple-point', color: '#e328ca', }), highlightSymbol: polylineSymbolSchema.prefault({ width: 5, color: defaultHighlightSymbolColor, }), dialogConfig: dialogWidgetConfigSchema.default({ id: 'MeasureElevationProfileDialog', width: '80vw', height: '30vh', y: '60vh', resizable: true, center: true, }), geoprocessor: z .object({ sampleCount: z.number().positive().optional(), sampleDistance: z.number().positive().optional(), }) .prefault({}), chart: z .object({ borderColor: colorSchema.optional(), backgroundColor: colorSchema.optional(), hoverBackgroundColor: colorSchema.default('#e328ca'), hoverRadius: z.number().default(5), maximumDataNumber: z.number().default(500), animation: z.boolean().default(true), }) .prefault({}), }) .prefault({}),});
export type MeasureElevationProfileWidgetConfig = z.infer<typeof measureElevationProfileConfigSchema>;export type MeasureElevationProfileConfig = MeasureElevationProfileWidgetConfig['config'];export type MeasureElevationProfileI18n = MeasureElevationProfileWidgetConfig['i18n'];packages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const measureElevationProfileI18n = { title: { fr: 'Tracer un profil altimétrique', nl: 'NL - Tracer un profil altimétrique', }, 'tab-title': { fr: 'Altimétrie', nl: 'NL - Altimétrie', }, 'layer-name': { fr: 'Mesure pour profil altimétrique', nl: 'NL - Mesure pour profil altimétrique', }, extend: { fr: 'Agrandir', nl: 'NL - Agrandir', }, 'hover-message': { fr: 'Alt: {{alt}} m; Dist: {{dist}} m', nl: 'NL - Alt: {{alt}} m; Dist: {{dist}} m', }, 'geoprocessing-error': { fr: 'Un problème a été rencontré lors de la génération du profil altimétrique: "{{error}}". Suggestion : assurez-vous de tracer votre profil exclusivement sur le territoire de la Wallonie...', nl: 'NL - Un problème a été rencontré lors de la génération du profil altimétrique: "{{error}}". Suggestion : assurez-vous de tracer votre profil exclusivement sur le territoire de la Wallonie...', }, 'geoprocessing-request-error': { fr: 'Un problème a été rencontré lors de la génération du profil altimétrique: "{{error}}".', nl: 'NL - Un problème a été rencontré lors de la génération du profil altimétrique: "{{error}}".', }, 'select-on-map': { fr: 'Sélectionner sur la carte', nl: 'NL - Sélectionner sur la carte', }, 'elevation-gain': { fr: 'Dénivelé', nl: 'NL - Dénivelé', }, positive: { fr: 'Positif', nl: 'NL - Positif', }, negative: { fr: 'Négatif', nl: 'NL - Négatif', }, 'meters-value': { fr: '{{value}} mètres', nl: 'NL - {{value}} mètres', },} satisfies I18nRegistry;packages/common/src/lib/widgets/measure/elevation-profile/measure-elevation-profile.model.ts
export type ChartData = { x: number; y: number; point: { x: number; y: number; };};
export type ElevationGain = { positiveGain: number; negativeGain: number;};packages/common/src/lib/widgets/measure/elevation-profile/MeasureElevationProfile.svelte
<script lang="ts"> import { initGraphicMapServiceConfiguration } from '$lib/api/managers/configuration'; import { getI18n } from '$lib/api/managers/i18n'; import { getMapManager } from '$lib/api/map'; import { onDestroy } from 'svelte'; import { SquareArrowOutUpRight, Pencil, MousePointerClick } from 'lucide-svelte'; import type { GeoprocessorResult, GeoprocessorSample } from '$lib/api/services/geoprocessor/geoprocessor.model'; import { Button } from '$lib/components/shadcn/ui/button'; import Dialog from '$lib/components/containers/dialog/Dialog.svelte'; import { getLayoutManager } from '$lib/api/managers/layout'; import MeasureElevationProfileChart from './MeasureElevationProfileChart.svelte'; import { type ChartData, type ElevationGain } from './measure-elevation-profile.model'; import { showToast } from '$lib/components/toast/toast.utils'; import type { MeasureElevationProfileProps } from './measure-elevation-profile.declaration'; import type { ApiFeature, ApiFeaturePoint, ApiFeaturePolyline } from '$lib/api/feature'; import { isEsriError } from '$lib/api/domain/esri.model'; import { ToggleGroup, ToggleGroupItem } from '$lib/components/shadcn/ui/toggle-group'; import type { Subscription } from '$lib/api/utils'; import { cleanSubscriptions } from '$lib/api/utils/index.js'; import type { ApiGeometryType } from '$lib/api/geometry'; import type { ApiGraphicsMapService } from '$lib/api/mapservices'; import { Label } from '$lib/components/shadcn/ui/label';
const { fullConfig }: MeasureElevationProfileProps = $props(); const { config } = fullConfig; const i18n = getI18n(fullConfig.i18n);
const layoutManager = getLayoutManager(); const mapManager = getMapManager(); const geoprocessor = mapManager.services.geoprocessor; const featureFactory = mapManager.tools.featureFactory; const drawFactory = mapManager.tools.draw; const eventsTool = mapManager.tools.events; const highlightTool = mapManager.tools.highlight;
const allowedGeometryType = 'polyline' satisfies ApiGeometryType; const serviceIdentifier = 'MeasureElevationProfileLayerId'; const graphicMapService = mapManager.addGraphicMapService( initGraphicMapServiceConfiguration({ label: i18n('layer-name'), id: serviceIdentifier, toc: { visible: false, }, }), ); const drawTool = drawFactory.create({ layer: graphicMapService });
let currentMode: 'draw' | 'select' = $state('draw');
let currentDraw: ApiFeaturePolyline | undefined; let currentWkid: number | undefined; let currentHoverPoint: ApiFeaturePoint | undefined;
let chartData: ChartData[] = $state([]); let elevationGain: ElevationGain | undefined = $state(undefined); let loading = $state(false);
let dialogOpen = $state(false); let dialogChart = $state<MeasureElevationProfileChart>();
let subscriptions: Subscription[] = [];
async function fetchElevationProfile(line: ApiFeaturePolyline) { loading = true; await geoprocessor .execute(line, config.geoprocessor) .then((geoprocessorResult) => onGeoprocessorResult(geoprocessorResult)) .catch((error: Error) => showToast({ level: 'error', message: i18n('geoprocessing-request-error', { error: error.message }), options: { duration: 5000, }, }), ) .finally(() => { loading = false; }); }
function onGeoprocessorResult(geoprocessorResult: GeoprocessorResult): void { if (isEsriError(geoprocessorResult)) { showToast({ level: 'error', message: i18n('geoprocessing-error', { error: geoprocessorResult.error.message }), options: { duration: 10000, }, }); } else { chartData = geoprocessorResult.samples.map((r) => ({ x: r.distance, y: r.altitude, point: { x: r.x, y: r.y }, })); elevationGain = calculateElevationGain(geoprocessorResult.samples); currentWkid = geoprocessorResult.wkid; } }
function removeCurrentHoverPoint() { if (currentHoverPoint) { graphicMapService.removeFeature(currentHoverPoint); } }
function showChartDataOnMap(chartData: ChartData): void { removeCurrentHoverPoint(); currentHoverPoint = featureFactory.createPoint({ ...chartData.point, wkid: currentWkid!, symbol: config.pointHoverSymbol, }); graphicMapService.addFeature(currentHoverPoint); }
function startDraw() { drawTool.create({ type: 'polyline', onDrawComplete: (evt) => setCurrentFeature(evt), symbol: config.polylineDrawSymbol, }); }
function onFeatureMapClick(resultMap: Map<ApiGraphicsMapService, ApiFeature[]>) { for (const features of resultMap.values()) { for (const feature of features) { if (feature !== currentDraw && feature.type === allowedGeometryType) { setCurrentFeature(feature); return; } } } }
function setCurrentFeature(feature: ApiFeaturePolyline) { if (currentDraw) { drawTool.delete(currentDraw); } removeCurrentHoverPoint(); currentDraw = feature; fetchElevationProfile(feature).then(() => { if (currentMode === 'draw') { startDraw(); } }); }
$effect(() => { if (currentMode === 'draw') { cleanSubscriptions(subscriptions); startDraw(); } else { drawTool.stop(); subscriptions.push( highlightTool.highlightHoveredFeatures({ geometryTypeRestriction: allowedGeometryType, exclude: graphicMapService, featureSymbols: { polyline: config.highlightSymbol }, }), eventsTool.listenToAnyGraphicMapServiceClick((result) => onFeatureMapClick(result)), ); } });
function calculateElevationGain(samples: GeoprocessorSample[]): ElevationGain { let positiveGain = 0; let negativeGain = 0; for (let i = 1; i < samples.length; i++) { const delta = samples[i].altitude - samples[i - 1].altitude; if (delta > 0) { positiveGain += delta; } else { negativeGain += Math.abs(delta); } } return { negativeGain: Math.round(negativeGain), positiveGain: Math.round(positiveGain), }; }
onDestroy(() => { cleanSubscriptions(subscriptions); drawTool.destroy(); mapManager.removeMapService(serviceIdentifier); });</script>
<div class="gv-p-4 gv-flex gv-flex-col gv-gap-3"> <p class="gv-text-xl gv-font-extrabold gv-border-b gv-border-grey-200">{i18n('title')}</p>
<ToggleGroup type="single" bind:value={currentMode} variant="outline" class="gv-justify-start"> <ToggleGroupItem value="draw" data-test-id="Elevation-Profil-Draw"> {i18n('common.draw')} <Pencil class="gv-size-3.5 gv-ml-2" /> </ToggleGroupItem> <ToggleGroupItem value="select" data-test-id="Elevation-Profil-Select"> {i18n('select-on-map')} <MousePointerClick class="gv-size-4 gv-ml-2" /> </ToggleGroupItem> </ToggleGroup>
<MeasureElevationProfileChart {config} i18nRegistry={fullConfig.i18n} {loading} data={chartData} onDataHover={(data) => showChartDataOnMap(data)} />
{#if elevationGain} <div class="gv-flex gv-flex-col"> <Label class="gv-font-extrabold">{i18n('elevation-gain')}</Label> <span class="gv-text-sm"> {i18n('positive')}: {i18n('meters-value', { value: elevationGain.positiveGain })} </span> <span class="gv-text-sm"> {i18n('negative')}: -{i18n('meters-value', { value: elevationGain.negativeGain })} </span> </div> {/if}
<Button onclick={() => (dialogOpen = true)} variant="outline" class="gv-w-fit" size="sm"> {i18n('extend')} <SquareArrowOutUpRight class="gv-h-4" /> </Button></div>
<Dialog open={dialogOpen} onclose={() => (dialogOpen = false)} onresize={() => dialogChart?.resize()} title={i18n('title')} {...config.dialogConfig} portal={layoutManager.layout.root}> <MeasureElevationProfileChart bind:this={dialogChart} {config} i18nRegistry={fullConfig.i18n} {loading} data={chartData} onDataHover={(data) => showChartDataOnMap(data)} ></MeasureElevationProfileChart></Dialog>packages/common/src/lib/widgets/measure/elevation-profile/MeasureElevationProfileChart.svelte
<script lang="ts"> import { getI18n } from '$lib/api/managers/i18n'; import { onDestroy, onMount } from 'svelte'; import { CategoryScale, Chart, Decimation, Filler, LinearScale, LineController, LineElement, PointElement, Tooltip, } from 'chart.js'; import type { MeasureElevationProfileConfig, MeasureElevationProfileI18n, } from '$lib/widgets/measure/elevation-profile/measure-elevation-profile.config'; import { LoaderCircle } from 'lucide-svelte'; import { getPrimaryColor, toString } from '$lib/api/utils'; import { getLayoutManager } from '$lib/api/managers/layout';
Chart.register(LineController, CategoryScale, LinearScale, PointElement, LineElement, Filler, Tooltip, Decimation);
type Props = { config: MeasureElevationProfileConfig; i18nRegistry: MeasureElevationProfileI18n; loading: boolean; data: ChartData[]; onDataHover: (chartData: ChartData) => void; };
const { config, i18nRegistry, loading, data, onDataHover }: Props = $props(); const i18n = getI18n(i18nRegistry);
const layoutManager = getLayoutManager();
let canvas = $state<HTMLCanvasElement>()!; let chart: Chart | undefined;
type ChartData = { x: number; y: number; point: { x: number; y: number; }; };
function initializeChart(): void { const context = canvas.getContext('2d'); if (context) { chart = new Chart(context, { type: 'line', data: { datasets: [ { label: 'Altitude', backgroundColor: toString( config.chart.backgroundColor ?? getComputedStyle(layoutManager.layout.root!).getPropertyValue('--spw-grey-200'), ), borderColor: toString( config.chart.borderColor ?? getComputedStyle(layoutManager.layout.root!).getPropertyValue('--spw-grey-400'), ), borderWidth: 2, hoverBackgroundColor: toString(config.chart.hoverBackgroundColor), hoverRadius: config.chart.hoverRadius, hoverBorderColor: '#000', fill: true, data: $state.snapshot(data), }, ], }, options: { parsing: false, maintainAspectRatio: false, animation: config.chart.animation ? undefined : false, elements: { line: { tension: 1 }, point: { radius: 1 } }, scales: { x: { type: 'linear', ticks: { maxTicksLimit: 10, includeBounds: false, }, title: { display: true, text: 'Distance (m)', }, }, y: { type: 'linear', ticks: { maxTicksLimit: 10, includeBounds: false, }, title: { display: true, text: 'Altitude (m)', }, }, }, interaction: { mode: 'index', intersect: false, }, hover: { mode: 'index', intersect: false, }, plugins: { tooltip: { enabled: true, animation: false, callbacks: { title: function () { return ''; }, label: function (tooltipItem) { const chartData = tooltipItem.raw as ChartData; return i18n('hover-message', { alt: chartData.y.toFixed(2), dist: chartData.x.toFixed(2), }); }, beforeLabel(tooltipItem) { onDataHover(tooltipItem.raw as ChartData); }, }, }, decimation: { enabled: true, algorithm: 'lttb', samples: config.chart.maximumDataNumber, }, }, }, }); } }
$effect(() => { if (data && chart) { const snapData = $state.snapshot(data); chart.data.datasets[0].data = snapData; chart.options.scales!.x!.max = Math.max(...snapData.map((d) => d.x)); chart.update(); } });
export function resize() { chart?.resize(); }
onMount(() => { initializeChart(); });
onDestroy(() => { chart?.destroy(); });</script>
<div class="gv-relative gv-h-full gv-overflow-hidden" class:gv-opacity-50={loading} class:gv-pointer-events-none={loading}> {#if loading} <LoaderCircle class="gv-animate-spin gv-absolute gv-inset-0 gv-m-auto gv-h-1/5 gv-w-1/5"></LoaderCircle> {/if} <canvas bind:this={canvas}></canvas></div>