Skip to content

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

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

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

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

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

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

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>

Aller plus loin