starts transition to maplibre
This commit is contained in:
218
web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts
vendored
Normal file
218
web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
import { type ControlPosition, type IControl } from "maplibre-gl";
|
||||
import * as M from "maplibre-gl";
|
||||
import { ElevationProfile, type ElevationProfileOptions } from "./elevationprofile";
|
||||
// @ts-ignore
|
||||
import type { GeoJsonObject } from "geojson";
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
|
||||
/**
|
||||
* Elevation profile control options
|
||||
*/
|
||||
export type ElevationProfileControlOptions = ElevationProfileOptions & {
|
||||
/**
|
||||
* If `true`, the elevation profile control will be visible as soon as it's ready.
|
||||
* If `false`, a click on the control button (or a programmatic call to `.showProfile()`)
|
||||
* will be neccesary to show the profile.
|
||||
*
|
||||
* Default: `false`
|
||||
*/
|
||||
visible?: boolean;
|
||||
/**
|
||||
* Size of the profile as a CSS rule.
|
||||
* This `size` will be the `width` if the `.position` is "left" or "right",
|
||||
* and will be the `height` if the `.position` is "top" or "bottom".
|
||||
*
|
||||
* Default: `"30%"`
|
||||
*/
|
||||
size?: string;
|
||||
/**
|
||||
* Position of the elevation profile chart when shown.
|
||||
*
|
||||
* Default: `"botton"`
|
||||
*/
|
||||
position?: "top" | "left" | "right" | "bottom";
|
||||
/**
|
||||
* Show the control button. If can be handy to hide it, especially if the profile is displayed
|
||||
* in a custom container and that its visiblity is managed by logic external to this control.
|
||||
*
|
||||
* Default: `true`
|
||||
*/
|
||||
showButton?: boolean;
|
||||
/**
|
||||
* A CSS class to add to the container. This is especially relevant when the options `.container` is not provided.
|
||||
* Important: if provided, no styling is added by this control and even placement will have to be managed by external CSS.
|
||||
*
|
||||
* Default: `""`
|
||||
*/
|
||||
containerClass?: string;
|
||||
|
||||
/**
|
||||
* DIV element to contain the control.
|
||||
* Important: if provided, no styling is added by this control.
|
||||
* Default: automatically created inside the map container
|
||||
*/
|
||||
container?: string | HTMLDivElement;
|
||||
};
|
||||
|
||||
export class ElevationProfileControl implements IControl {
|
||||
private map?: M.Map;
|
||||
private buttonContainer?: HTMLDivElement;
|
||||
private toggleButton?: HTMLButtonElement;
|
||||
private isProfileShown = false;
|
||||
private iconSpan?: HTMLSpanElement;
|
||||
|
||||
private profileContainer?: HTMLDivElement;
|
||||
private settings: ElevationProfileControlOptions;
|
||||
private data: GeoJsonObject | null = null;
|
||||
private elevationProfileChart?: ElevationProfile;
|
||||
|
||||
constructor(options: ElevationProfileControlOptions = {}) {
|
||||
if (typeof window === "undefined")
|
||||
throw new Error("This pluggin must be mounted client-side");
|
||||
this.settings = { ...options };
|
||||
}
|
||||
|
||||
toggleTheme(options: ElevationProfileControlOptions) {
|
||||
this.settings = { ...this.settings, ...options };
|
||||
this.elevationProfileChart?.toggleTheme(this.settings)
|
||||
}
|
||||
|
||||
getContainer(): HTMLDivElement | undefined {
|
||||
return this.profileContainer;
|
||||
}
|
||||
|
||||
onAdd(map: M.Map): HTMLElement {
|
||||
this.map = map;
|
||||
|
||||
this.buttonContainer = document.createElement("div");
|
||||
|
||||
if (this.settings.showButton === false) {
|
||||
this.buttonContainer.style.setProperty("display", "none");
|
||||
}
|
||||
|
||||
this.buttonContainer.classList.add(
|
||||
"maplibregl-ctrl",
|
||||
"maplibregl-ctrl-group"
|
||||
);
|
||||
this.toggleButton = document.createElement("button");
|
||||
this.buttonContainer.appendChild(this.toggleButton);
|
||||
this.iconSpan = document.createElement("i");
|
||||
this.iconSpan.classList.add("fa", "fa-chart-line", "text-black");
|
||||
this.toggleButton.appendChild(this.iconSpan);
|
||||
this.toggleButton.addEventListener("click", this.toggleProfile.bind(this));
|
||||
const mapContainer = map.getContainer();
|
||||
const size = this.settings.size ?? "30%";
|
||||
|
||||
if (this.settings.container) {
|
||||
const tmpContainer =
|
||||
typeof this.settings.container === "string"
|
||||
? document.getElementById(this.settings.container)
|
||||
: this.settings.container;
|
||||
if (!tmpContainer) throw new Error("The provided container is invalid");
|
||||
this.profileContainer = tmpContainer as HTMLDivElement;
|
||||
} else {
|
||||
this.profileContainer = document.createElement("div");
|
||||
this.profileContainer.style.setProperty("display", "none");
|
||||
|
||||
if (!this.settings.containerClass) {
|
||||
this.profileContainer.classList.add(this.settings.backgroundColor ?? "white")
|
||||
|
||||
this.profileContainer.style.setProperty("position", "absolute");
|
||||
|
||||
if (this.settings.position === "bottom" || !this.settings.position) {
|
||||
// To prevent clashing with MapTiler logo and attribution control
|
||||
this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
|
||||
this.profileContainer.style.setProperty("width", "100%");
|
||||
this.profileContainer.style.setProperty("height", size);
|
||||
this.profileContainer.style.setProperty("bottom", "0");
|
||||
} else if (this.settings.position === "top") {
|
||||
this.profileContainer.style.setProperty("width", "100%");
|
||||
this.profileContainer.style.setProperty("height", size);
|
||||
this.profileContainer.style.setProperty("top", "0");
|
||||
} else if (this.settings.position === "left") {
|
||||
// To prevent clashing with MapTiler logo and attribution control
|
||||
this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
|
||||
this.profileContainer.style.setProperty("width", size);
|
||||
this.profileContainer.style.setProperty("height", "100%");
|
||||
this.profileContainer.style.setProperty("left", "0");
|
||||
} else if (this.settings.position === "right") {
|
||||
// To prevent clashing with MapTiler logo and attribution control
|
||||
this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
|
||||
this.profileContainer.style.setProperty("width", size);
|
||||
this.profileContainer.style.setProperty("height", "100%");
|
||||
this.profileContainer.style.setProperty("right", "0");
|
||||
}
|
||||
}
|
||||
|
||||
const waypointContainer = document.createElement("div")
|
||||
waypointContainer.className = "absolute w-full"
|
||||
waypointContainer.id = "waypoint-container"
|
||||
|
||||
this.profileContainer.append(waypointContainer);
|
||||
|
||||
mapContainer.appendChild(this.profileContainer);
|
||||
}
|
||||
|
||||
if (this.settings.containerClass) {
|
||||
this.profileContainer.classList.add(this.settings.containerClass);
|
||||
}
|
||||
|
||||
this.elevationProfileChart = new ElevationProfile(
|
||||
this.profileContainer,
|
||||
this.settings
|
||||
);
|
||||
|
||||
if (this.settings.visible) {
|
||||
this.showProfile();
|
||||
}
|
||||
|
||||
return this.buttonContainer;
|
||||
}
|
||||
|
||||
private toggleProfile() {
|
||||
if (!this.profileContainer) return;
|
||||
|
||||
if (this.isProfileShown) {
|
||||
this.hideProfile();
|
||||
} else {
|
||||
this.showProfile();
|
||||
}
|
||||
}
|
||||
|
||||
showProfile() {
|
||||
this.profileContainer?.style.setProperty("display", "inherit");
|
||||
this.isProfileShown = true;
|
||||
}
|
||||
|
||||
hideProfile() {
|
||||
this.profileContainer?.style.setProperty("display", "none");
|
||||
this.isProfileShown = false;
|
||||
}
|
||||
|
||||
onRemove(): void {
|
||||
// remove button
|
||||
if (this.buttonContainer?.parentNode) {
|
||||
this.buttonContainer.parentNode.removeChild(this.buttonContainer);
|
||||
}
|
||||
this.map = undefined;
|
||||
this.buttonContainer = undefined;
|
||||
this.toggleButton = undefined;
|
||||
this.isProfileShown = false;
|
||||
}
|
||||
|
||||
getDefaultPosition?: (() => ControlPosition) | undefined;
|
||||
|
||||
async setData(data: GeoJsonObject, waypoints?: Waypoint[]) {
|
||||
if (!this.map || !this.elevationProfileChart) {
|
||||
throw new Error(
|
||||
"The Elevation Profile Control needs to be mounted on a map instance before setting any data."
|
||||
);
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
|
||||
if (!this.data) return;
|
||||
|
||||
this.elevationProfileChart.setData(this.data, waypoints);
|
||||
}
|
||||
}
|
||||
1006
web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts
vendored
Normal file
1006
web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
47
web/src/lib/vendor/maplibre-elevation-profile/tools.ts
vendored
Normal file
47
web/src/lib/vendor/maplibre-elevation-profile/tools.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import { haversineDistance } from "$lib/models/gpx/utils";
|
||||
import type {
|
||||
Position
|
||||
} from "geojson";
|
||||
|
||||
|
||||
export function haversineCumulatedDistanceWgs84(path: Position[]): number[] {
|
||||
if (path.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let totalDistance = 0;
|
||||
const distances: number[] = [0];
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const [lon1, lat1] = path[i];
|
||||
const [lon2, lat2] = path[i + 1];
|
||||
|
||||
totalDistance += haversineDistance(lat1, lon1, lat2, lon2);
|
||||
distances.push(totalDistance);
|
||||
}
|
||||
|
||||
return distances; // Array of cumulative distances in meters
|
||||
}
|
||||
|
||||
export function smoothElevations(positions: Position[], windowSize: number): Position[] {
|
||||
// Ensure windowSize is valid (at least 1)
|
||||
if (windowSize < 1) throw new Error("Window size must be at least 1.");
|
||||
|
||||
// Create a new array with smoothed elevations
|
||||
return positions.map((pos, i, arr) => {
|
||||
const start = Math.max(0, i - Math.floor(windowSize / 2)); // Start index for the window
|
||||
const end = Math.min(arr.length, i + Math.floor(windowSize / 2) + 1); // End index for the window
|
||||
const segment = arr.slice(start, end); // Extract the positions in the window
|
||||
|
||||
// Calculate the weighted moving average of elevations
|
||||
const weights = segment.map((_, idx) => idx + 1); // Increasing weights: 1, 2, 3...
|
||||
const elevations = segment.map(p => p[2]); // Extract elevations
|
||||
const weightedSum = elevations.reduce((sum, elevation, idx) => sum + elevation * weights[idx], 0);
|
||||
const weightTotal = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
|
||||
const smoothedElevation = weightedSum / weightTotal; // Weighted average elevation
|
||||
|
||||
// Return a new Position with the smoothed elevation
|
||||
return [pos[0], pos[1], smoothedElevation] as Position;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user