cleanup of obsolote leaflet components

This commit is contained in:
Christian Beutel
2024-11-30 01:13:57 +01:00
parent 8c42e9e695
commit 8f8d28ef7a
56 changed files with 118 additions and 9198 deletions

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { SummitLog } from "$lib/models/summit_log"; import type { SummitLog } from "$lib/models/summit_log";
import "leaflet/dist/leaflet.css";
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import Modal from "../base/modal.svelte"; import Modal from "../base/modal.svelte";
import SummitLogTableRow from "./summit_log_table_row.svelte"; import SummitLogTableRow from "./summit_log_table_row.svelte";

View File

@@ -1,250 +0,0 @@
<script lang="ts">
import { page } from "$app/stores";
import { Settings } from "$lib/models/settings";
import type { Trail } from "$lib/models/trail";
import {
createMarkerFromWaypoint,
endIcon,
startIcon,
} from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import type { Map, Marker } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import { createEventDispatcher, onMount } from "svelte";
import { _ } from "svelte-i18n";
import Dropdown from "../base/dropdown.svelte";
export let trail: Trail | null;
export let markers: Marker[] = [];
export let map: Map | null = null;
export let options: any = {};
export let graticule: AutoGraticule | null = null;
export let crosshair: boolean = false;
const dispatch = createEventDispatcher();
let L: any;
let controlElevation: any;
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
$: gpxData = trail?.expand.gpx_data;
$: if (gpxData && controlElevation) {
controlElevation.updateOptions({
autofitBounds: options.autofitBounds ?? true,
});
controlElevation.clear();
controlElevation.load(gpxData);
}
$: if (options) {
controlElevation?.updateOptions(options);
}
$: hotlineSwitcherItems = [
{
text: $_("altitude"),
value: "altitude",
icon: selectedMetric == "altitude" ? "circle-dot" : "circle",
},
{
text: $_("slope"),
value: "slope",
icon: selectedMetric == "slope" ? "circle-dot" : "circle",
},
{
text: $_("speed"),
value: "speed",
icon: selectedMetric == "speed" ? "circle-dot" : "circle",
},
{
text: $_("off"),
value: false,
icon: selectedMetric == false ? "circle-dot" : "circle",
},
];
onMount(async () => {
L = (await import("leaflet")).default;
await import("leaflet-gpx");
await import("leaflet.awesome-markers");
//@ts-ignore
await import("$lib/vendor/leaflet-elevation/src/index.js");
const AutoGraticule = (
await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule")
).default;
map = L.map("map", { preferCanvas: true }).setView(
[trail?.lat ?? 0, trail?.lon ?? 0],
3,
);
map!.attributionControl.setPrefix(false);
map!.on("zoomend", function () {
dispatch("zoomend", map);
});
map!.on("click", function (e) {
dispatch("click", e);
});
const baseLayer = L.tileLayer(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{
attribution: "© OpenStreetMap contributors",
},
);
const topoLayer = L.tileLayer(
"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
{
attribution: "© OpenStreetMap contributors",
},
);
const baseMaps: Record<string, L.TileLayer> = {
OpenStreetMaps: baseLayer,
OpenTopoMaps: topoLayer,
...($page.data.settings as Settings)?.tilesets?.reduce<
Record<string, string>
>((t, current) => {
t[current.name] = L.tileLayer(current.url);
return t;
}, {}),
};
L.control.layers(baseMaps).addTo(map);
const layerPreference = localStorage.getItem("layer");
if (
layerPreference &&
Object.keys(baseMaps).includes(layerPreference)
) {
baseMaps[layerPreference].addTo(map!);
} else {
baseLayer.addTo(map);
}
map!.on("baselayerchange", function (e) {
localStorage.setItem("layer", e.name);
});
const localMetric =
(localStorage.getItem("gradient") as any) ?? "altitude";
selectedMetric = localMetric === "false" ? false : localMetric;
const default_elevation_options = {
height: 200,
theme: "lightblue-theme",
detached: true,
elevationDiv: "#elevation",
closeBtn: false,
followMarker: true,
autofitBounds: true,
imperial: ($page.data.settings?.unit ?? "metric") == "imperial",
reverseCoords: false,
acceleration: false,
slope: true,
speed: true,
altitude: true,
time: true,
distance: true,
// Summary track info style: "inline" || "multiline" || false
summary: false,
downloadLink: false,
ruler: false,
legend: true,
// Toggle "leaflet-almostover" integration
almostOver: false,
// Toggle "leaflet-distance-markers" integration
distanceMarkers: {
lazy: false,
distance: false,
direction: true,
offset: 500,
},
// Toggle "leaflet-edgescale" integration
edgeScale: false,
// Toggle "leaflet-hotline" integration
hotline: selectedMetric,
// Display track datetimes: true || false
timestamps: false,
waypoints: false,
wptIcons: false,
wptLabels: false,
preferCanvas: true,
trkStart: {
interactive: false,
className: "hihi",
icon: startIcon(),
},
trkEnd: {
icon: endIcon(),
},
graticule: false,
drawing: false,
};
const elevation_options = Object.assign(
default_elevation_options,
options,
);
controlElevation = L.control.elevation(elevation_options).addTo(map);
for (const waypoint of trail?.expand.waypoints ?? []) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(map!);
markers.push(marker);
}
if (elevation_options.graticule) {
graticule = new AutoGraticule();
graticule.addTo(map!);
}
});
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
controlElevation.updateOptions({
hotline: metric,
autofitBounds: false,
});
selectedMetric = metric;
localStorage.setItem("gradient", metric.toString());
controlElevation.clear();
controlElevation.load(gpxData);
}
</script>
<div id="map-container" class="flex flex-col h-full relative">
<div
id="map"
class="rounded-xl z-0 basis-full min-h-96 md:min-h-0"
style={crosshair
? "position: relative; outline-style: none;cursor: crosshair !important"
: "position: relative; outline-style: none;"}
>
<div class="absolute top-20 right-3 text-sm" style="z-index: 500">
<Dropdown
items={hotlineSwitcherItems}
on:change={(e) => switchHotline(e.detail.value)}
let:toggleMenu={openDropdown}
>
<button
class="rounded-md border-2 border-black border-opacity-30 bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
on:click={openDropdown}
>
<i class="fa fa-route text-lg"></i>
</button>
</Dropdown>
</div>
</div>
<div class="flex items-center justify-between">
<slot />
<div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>
</div>
</div>

View File

@@ -16,6 +16,7 @@
import "maplibre-gl/dist/maplibre-gl.css"; import "maplibre-gl/dist/maplibre-gl.css";
import { createEventDispatcher, onDestroy, onMount } from "svelte"; import { createEventDispatcher, onDestroy, onMount } from "svelte";
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule"; import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
export let trails: Trail[] = []; export let trails: Trail[] = [];
export let markers: M.Marker[] = []; export let markers: M.Marker[] = [];
@@ -24,6 +25,8 @@
export let showElevation: boolean = true; export let showElevation: boolean = true;
export let showInfoPopup: boolean = false; export let showInfoPopup: boolean = false;
export let showGrid: boolean = false; export let showGrid: boolean = false;
export let showStyleSwitcher: boolean = true;
export let showFullscreen: boolean = false;
export let fitBounds: "animate" | "instant" | "off" = "instant"; export let fitBounds: "animate" | "instant" | "off" = "instant";
export let elevationProfileContainer: string | HTMLDivElement | undefined = export let elevationProfileContainer: string | HTMLDivElement | undefined =
@@ -520,7 +523,18 @@
}); });
map.addControl(epc); map.addControl(epc);
} }
if (showStyleSwitcher) {
map.addControl(switcherControl); map.addControl(switcherControl);
}
if (showFullscreen) {
map.addControl(
new FullscreenControl(() => {
dispatch("fullscreen");
}),
"bottom-right",
);
}
map.on("styledata", () => { map.on("styledata", () => {
trails.forEach((t, i) => { trails.forEach((t, i) => {

View File

@@ -1,355 +0,0 @@
<script lang="ts">
import { page } from "$app/stores";
import { Settings } from "$lib/models/settings";
import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import { createEventDispatcher, onMount } from "svelte";
import { _ } from "svelte-i18n";
import Dropdown from "../base/dropdown.svelte";
export let trails: Trail[];
export let map: any | null = null;
export let options: any = {};
export let activeTrailIndex: number | null = 0;
export let markers: any[] = [];
export let bindRoutePopup: boolean = true;
const dispatch = createEventDispatcher();
let L: any;
let gpxGroup: any;
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
$: gpxData = trails.map((t) => {
return { id: t.id, gpx: t.expand.gpx_data };
});
$: if (
gpxData &&
gpxGroup &&
map &&
(gpxData.length != gpxGroup._tracks.length ||
gpxGroup?._tracks[0] !== gpxData[0])
) {
// if (gpxData.length == 0) {
// map?.setView([0, 0], 4);
// }
gpxGroup._elevation.updateOptions({
autofitBounds: options.autofitBounds ?? true,
});
gpxGroup.clear();
gpxGroup._tracks = gpxData;
gpxGroup.addTracks();
}
$: if (options && gpxGroup) {
gpxGroup._elevation.updateOptions(options);
}
$: hotlineSwitcherItems = [
{
text: $_("altitude"),
value: "altitude",
icon: selectedMetric == "altitude" ? "circle-dot" : "circle",
},
{
text: $_("slope"),
value: "slope",
icon: selectedMetric == "slope" ? "circle-dot" : "circle",
},
{
text: $_("speed"),
value: "speed",
icon: selectedMetric == "speed" ? "circle-dot" : "circle",
},
{
text: $_("off"),
value: false,
icon: selectedMetric == false ? "circle-dot" : "circle",
},
];
onMount(async () => {
L = (await import("leaflet")).default;
await import("leaflet-gpx");
await import("leaflet.awesome-markers");
await import("$lib/vendor/leaflet-elevation/src/index.js");
await import("$lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup");
map = L.map("map", {
preferCanvas: true,
plugins: ["/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js"],
}).setView(
[
trails.at(activeTrailIndex ?? 0)?.lat ?? 0,
trails.at(activeTrailIndex ?? 0)?.lon ?? 0,
],
3,
);
dispatch("init", map);
map!.attributionControl.setPrefix(false);
map!.on("zoomend", function () {
dispatch("zoomend", map);
});
map!.on("click", function (e: any) {
dispatch("click", e);
});
map!.on("moveend", function (e: any) {
dispatch("moveend", e);
});
const baseLayer = L.tileLayer(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{
attribution: "© OpenStreetMap contributors",
},
);
const topoLayer = L.tileLayer(
"https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
{
attribution: "© OpenStreetMap contributors",
},
);
const baseMaps: Record<string, L.TileLayer> = {
OpenStreetMaps: baseLayer,
OpenTopoMaps: topoLayer,
...($page.data?.settings as Settings)?.tilesets?.reduce<
Record<string, string>
>((t, current) => {
t[current.name] = L.tileLayer(current.url);
return t;
}, {}),
};
L.control.layers(baseMaps).addTo(map);
const layerPreference = localStorage.getItem("layer");
if (
layerPreference &&
Object.keys(baseMaps).includes(layerPreference)
) {
baseMaps[layerPreference].addTo(map!);
} else {
baseLayer.addTo(map);
}
map!.on("baselayerchange", function (e: any) {
localStorage.setItem("layer", e.name);
});
const localMetric =
(localStorage.getItem("gradient") as any) ?? "altitude";
selectedMetric = localMetric === "false" ? false : localMetric;
const default_elevation_options = {
height: 200,
theme: "lightblue-theme",
detached: true,
elevationDiv: "#elevation",
closeBtn: false,
followMarker: true,
autofitBounds: true,
imperial: $page.data.settings?.unit == "imperial",
reverseCoords: false,
acceleration: false,
slope: true,
speed: true,
altitude: true,
time: true,
distance: true,
// Summary track info style: "inline" || "multiline" || false
summary: false,
downloadLink: false,
ruler: false,
legend: true,
// Toggle "leaflet-almostover" integration
almostOver: false,
// Toggle "leaflet-distance-markers" integration
distanceMarkers: {
lazy: false,
distance: false,
direction: true,
offset: 1000,
},
// Toggle "leaflet-edgescale" integration
edgeScale: false,
// Toggle "leaflet-hotline" integration
hotline: selectedMetric,
// Display track datetimes: true || false
timestamps: false,
waypoints: false,
wptIcons: false,
wptLabels: false,
preferCanvas: true,
graticule: false,
drawing: false,
};
const elevation_options = Object.assign(
default_elevation_options,
options,
);
gpxGroup = L.gpxGroup(gpxData, {
points: [],
elevation: true,
elevation_options: elevation_options,
flyToBounds: options.flyToBounds,
distanceMarkers: true,
itinerary: options.itinerary,
});
const markerLayerGroup = L.layerGroup().addTo(map);
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
markerLayerGroup.clearLayers();
activeTrailIndex = null;
markers = [];
if (polyline && polyline._selected) {
activeTrailIndex = trails.findIndex(
(t) => t.id == polyline.options.id,
);
for (const waypoint of trails.at(activeTrailIndex!)?.expand
.waypoints ?? []) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(markerLayerGroup!);
markers.push(marker);
}
dispatch(
"select",
trails.find((t) => t.id == polyline.options.id),
);
}
});
gpxGroup.on("clear", () => {
markerLayerGroup.clearLayers();
activeTrailIndex = null;
});
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
const trail = trails.find((t) => t.id == route.options.id);
if (!trail) {
return;
}
const thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp";
if (bindRoutePopup) {
route.bindPopup(
`<a href="/trail/view/${trail.id}" data-sveltekit-preload-data="off">
<li class="flex items-center gap-4 cursor-pointer text-black max-w-80">
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${thumbnail}" alt="">
</div>
<div>
<h4 class="font-semibold text-lg">${trail.name}</h4>
<div class="flex gap-x-4">
${trail.location ? `<h5><i class="fa fa-location-dot mr-2"></i>${trail.location}</h5>` : ""}
<h5><i class="fa fa-gauge mr-2"></i>${$_(trail.difficulty as string)}</h5>
</div>
<div class="grid grid-cols-2 mt-2 gap-x-4 gap-y-2 text-sm text-gray-500 flex-wrap"><span class="shrink-0"><i
class="fa fa-left-right mr-2"></i>${formatDistance(
trail.distance,
)}</span><span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
trail.duration,
)}</span><span class="shrink-0"><i class="fa fa-arrow-trend-up mr-2"></i>${formatElevation(
trail.elevation_gain,
)}</span></span> <span class="shrink-0"><i class="fa fa-arrow-trend-down mr-2"></i>${formatElevation(
trail.elevation_loss,
)}</span></div>
</div>
</li>
</a>`,
);
}
});
gpxGroup.addTo(map);
});
export function highlightTrail(id: string) {
gpxGroup?.highlightTrack(id);
}
export function unHighlightTrail(id: string) {
gpxGroup?.unHighlightTrack(id);
}
export function selectTrail(id: string) {
gpxGroup?.select(id);
}
export function resetSelection() {
gpxGroup?.resetSelection();
}
export function openPopup(id: string) {
gpxGroup?.openPopup(id);
}
export function closePopup(id: string) {
gpxGroup?.closePopup(id);
}
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {
if (metric === selectedMetric) {
return;
}
gpxGroup._elevation.updateOptions({
hotline: metric,
});
gpxGroup.options.flyToBounds = false;
selectedMetric = metric;
localStorage.setItem("gradient", metric.toString());
gpxGroup.clear();
gpxGroup._tracks = gpxData;
gpxGroup.addTracks();
gpxGroup.options.flyToBounds = true;
}
</script>
<div id="map-container" class="h-full relative">
<div
id="map"
class="rounded-xl z-0 h-full min-h-96 md:min-h-0"
style="position: relative; outline-style: none;"
>
<div class="absolute top-20 right-3 text-sm" style="z-index: 500">
<Dropdown
items={hotlineSwitcherItems}
on:change={(e) => switchHotline(e.detail.value)}
let:toggleMenu={openDropdown}
>
<button
class="rounded-md border-2 border-black border-opacity-30 bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
on:click={openDropdown}
>
<i class="fa fa-route text-lg"></i>
</button>
</Dropdown>
</div>
</div>
<div class="absolute bottom-0 bg-background/80 w-full" id="elevation"></div>
</div>

View File

@@ -20,15 +20,7 @@
formatElevation, formatElevation,
formatTimeHHMM, formatTimeHHMM,
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import {
createMarkerFromWaypoint,
endIcon,
startIcon,
} from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import type { Map, Marker } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import "photoswipe/style.css"; import "photoswipe/style.css";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
@@ -40,10 +32,11 @@
import SummitLogTable from "../summit_log/summit_log_table.svelte"; import SummitLogTable from "../summit_log/summit_log_table.svelte";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
import MapWithElevationMaplibre from "./map_with_elevation_maplibre.svelte";
export let trail: Trail; export let trail: Trail;
export let mode: "overview" | "map" | "list" = "map"; export let mode: "overview" | "map" | "list" = "map";
export let markers: (Marker | M.Marker)[] = []; export let markers: M.Marker[] = [];
const tabs = [ const tabs = [
$_("description"), $_("description"),
@@ -56,8 +49,6 @@
const trailIsShared = const trailIsShared =
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0; (trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
let map: Map;
let activeTab = 0; let activeTab = 0;
let thumbnail = trail.photos.length let thumbnail = trail.photos.length
@@ -74,65 +65,15 @@
fetchComments(); fetchComments();
} }
onMount(async () => { onMount(async () => {});
if (mode == "overview") {
const L = (await import("leaflet")).default;
await import("leaflet-gpx");
await import("leaflet.awesome-markers");
map = L.map("map").setView([0, 0], 2);
map.attributionControl.setPrefix(false);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(map);
const gpxLayer = new L.GPX(trail.expand.gpx_data!, {
async: true,
polyline_options: {
className: "lightblue-theme elevation-polyline",
opacity: 0.75,
weight: 5,
},
gpx_options: {
parseElements: ["track", "route"] as any,
},
marker_options: {
startIcon: startIcon() as any,
startIconUrl: "",
endIcon: endIcon() as any,
endIconUrl: "",
shadowUrl: "",
},
})
.on("loaded", function (e) {
map.fitBounds(e.target.getBounds());
})
.addTo(map);
for (const waypoint of trail.expand.waypoints) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(map);
markers.push(marker);
}
}
});
function openMarkerPopup(i: number) { function openMarkerPopup(i: number) {
if (typeof (markers[i] as Marker<any>).openPopup === "function") {
(markers[i] as Marker<any>).openPopup();
} else {
(markers[i] as M.Marker).togglePopup(); (markers[i] as M.Marker).togglePopup();
} }
}
function closeMarkerPopup(i: number) { function closeMarkerPopup(i: number) {
if (typeof (markers[i] as Marker<any>).closePopup === "function") {
(markers[i] as Marker<any>).closePopup();
} else {
(markers[i] as M.Marker).togglePopup(); (markers[i] as M.Marker).togglePopup();
} }
}
async function toggleMapFullScreen() { async function toggleMapFullScreen() {
goto(`/map/trail/${trail.id!}`); goto(`/map/trail/${trail.id!}`);
@@ -425,16 +366,16 @@
</div> </div>
{/if} {/if}
{#if mode == "overview"} {#if mode == "overview"}
<div class="relative"> <div class="relative h-72 rounded-xl">
<div class="rounded-xl h-72" id="map"> <MapWithElevationMaplibre
<div class="leaflet-top leaflet-right"> trails={[trail]}
<button showElevation={false}
class="leaflet-control fa fa-maximize rounded-full text-lg bg-white text-black px-[14px] py-2 hover:bg-gray-100" showStyleSwitcher={false}
style="cursor: pointer !important" showFullscreen={true}
on:click={() => toggleMapFullScreen()} mapOptions={{ attributionControl: false }}
></button> on:fullscreen={toggleMapFullScreen}
</div> bind:markers
</div> ></MapWithElevationMaplibre>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -13,8 +13,8 @@
import TextField from "../base/text_field.svelte"; import TextField from "../base/text_field.svelte";
import Textarea from "../base/textarea.svelte"; import Textarea from "../base/textarea.svelte";
import PhotoPicker from "../trail/photo_picker.svelte"; import PhotoPicker from "../trail/photo_picker.svelte";
import { convertDMSToDD } from "$lib/util/leaflet_util";
import { show_toast } from "$lib/stores/toast_store"; import { show_toast } from "$lib/stores/toast_store";
import { convertDMSToDD } from "$lib/models/gpx/utils";
export let openModal: (() => void) | undefined = undefined; export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined;

View File

@@ -33,4 +33,13 @@ function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
return distance; return distance;
} }
export { removeEmpty, allDatesToISOString, haversineDistance }; function convertDMSToDD(dms: Number[], direction: "N" | "O" | "S" | "W") {
var dd = dms[0].valueOf() + dms[1].valueOf() / 60 + dms[2].valueOf() / (60 * 60);
if (direction == "S" || direction == "W") {
dd = dd * -1;
}
return dd;
}
export { removeEmpty, allDatesToISOString, haversineDistance, convertDMSToDD };

View File

@@ -4,12 +4,11 @@ import type { Waypoint } from "$lib/models/waypoint";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { util } from "$lib/vendor/svelte-form-lib/util"; import { util } from "$lib/vendor/svelte-form-lib/util";
import type { LatLng } from "leaflet"; import * as M from "maplibre-gl";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store"; import { summit_logs_create, summit_logs_delete, summit_logs_update } from "./summit_log_store";
import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store"; import { waypoints_create, waypoints_delete, waypoints_update } from "./waypoint_store";
import * as M from "maplibre-gl";
export const trails: Writable<Trail[]> = writable([]) export const trails: Writable<Trail[]> = writable([])
export const trail: Writable<Trail> = writable(new Trail("")); export const trail: Writable<Trail> = writable(new Trail(""));

View File

@@ -5,6 +5,7 @@ import { getFileURL } from "./file_util";
import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util"; import { formatDistance, formatElevation, formatTimeHHMM } from "./format_util";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { haversineDistance } from "$lib/models/gpx/utils";
export class FontawesomeMarker extends M.Marker { export class FontawesomeMarker extends M.Marker {
constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) { constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string, id?: string }, markerOptions?: M.MarkerOptions) {
@@ -110,41 +111,42 @@ export function createPopupFromTrail(trail: Trail) {
return popup; return popup;
} }
// export function calculatePixelPerMeter(map: Map, meters: number) { export function calculatePixelPerMeter(map: M.Map, meters: number) {
// const y = map.getSize().y; const y = map.getCanvas().getBoundingClientRect().y;
// const x = map.getSize().x; const x = map.getCanvas().getBoundingClientRect().x;
// const maxMeters = map.containerPointToLatLng([0, y]).distanceTo(map.containerPointToLatLng([x, y])); const maxMeters = map.unproject([0, y]).distanceTo(map.unproject([x, y]));
// const pixelPerMeter = x / maxMeters; const pixelPerMeter = x / maxMeters;
// return pixelPerMeter * meters return pixelPerMeter * meters
// } }
// export function calculateScaleFactor(map: Map) { export function calculateScaleFactor(map: M.Map) {
// function _pxTOmm() { function _pxTOmm() {
// let heightRef = document.createElement('div'); let heightRef = document.createElement('div');
// heightRef.style.height = '1mm'; heightRef.style.height = '1mm';
// heightRef.style.position = "absolute"; heightRef.style.position = "absolute";
// heightRef.id = 'heightRef'; heightRef.id = 'heightRef';
// document.body.appendChild(heightRef); document.body.appendChild(heightRef);
// const pxPermm = heightRef.getBoundingClientRect().height; const pxPermm = heightRef.getBoundingClientRect().height;
// document.body.removeChild(heightRef); document.body.removeChild(heightRef);
// return function pxTOmm(px: number) { return function pxTOmm(px: number) {
// return px / pxPermm; return px / pxPermm;
// } }
// } }
// var centerOfMap = map.getSize().y / 2; var centerOfMap = map.getCanvas().getBoundingClientRect().y / 2;
// var realWorlMetersPer100Pixels = map.distance( const p1 = map.unproject([0, centerOfMap]);
// map.containerPointToLatLng([0, centerOfMap]), const p2 = map.unproject([100, centerOfMap]);
// map.containerPointToLatLng([100, centerOfMap]) var realWorldMetersPer100Pixels = haversineDistance(
// ); p1.lat, p1.lng, p2.lat, p2.lng
);
// const screenMetersPer100Pixels = _pxTOmm()(100) / 1000; const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
// const scaleFactor = realWorlMetersPer100Pixels / screenMetersPer100Pixels const scaleFactor = realWorldMetersPer100Pixels / screenMetersPer100Pixels
// return scaleFactor return scaleFactor
// } }

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.5 8.5"><defs><filter id="shadow"><feDropShadow dx="0" dy="0" stdDeviation="0.3" flood-color="#000000"/></filter></defs><circle cx="4.2" cy="4.2" r="2.2" fill="#ffffff" fill-opacity="1" filter="url(#shadow)"/><circle cx="4.2" cy="4.2" r="1.85" fill="none" stroke="#333333" stroke-linejoin="round" stroke-width="0.45"/><circle cx="4.2" cy="4.2" r="1.25" fill="none" stroke="#333333" stroke-linejoin="round" stroke-width="0.25"/><circle cx="4.2" cy="4.2" r="0.55" fill="#333333"/></svg>

Before

Width:  |  Height:  |  Size: 563 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,405 +0,0 @@
/*
* https://github.com/adoroszlai/joebed/tree/gh-pages
*
* The MIT License (MIT)
*
* Copyright (c) 2014- Doroszlai Attila, 2019- Raruto
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
L.Mixin.Selectable = {
includes: L.Mixin.Events,
setSelected: function (s) {
var selected = !!s;
if (this._selected !== selected) {
this._selected = selected;
this.fire('selected');
}
},
isSelected: function () {
return !!this._selected;
},
};
L.Mixin.Selection = {
includes: L.Mixin.Events,
getSelection: function () {
return this._selected;
},
setSelection: function (item) {
if (this._selected === item) {
if (item !== null) {
item.setSelected(!item.isSelected());
if (!item.isSelected()) {
this._selected = null;
}
}
} else {
if (this._selected) {
this._selected.setSelected(false);
}
this._selected = item;
if (this._selected) {
this._selected.setSelected(true);
}
}
this.fire('selection_changed', { polyline: item });
},
};
L.GeoJSON.include(L.Mixin.Selectable);
export const GpxGroup = L.GpxGroup = L.Class.extend({
options: {
highlight: {
opacity: 1,
weight: 6,
},
points: [],
points_options: {
icon: {
iconUrl: '../images/elevation-poi.png',
iconSize: [12, 12],
}
},
flyToBounds: true,
elevation: true,
elevation_options: {
theme: 'lightblue-theme',
detached: true,
elevationDiv: '#elevation',
},
distanceMarkers: true,
distanceMarkers_options: {
lazy: true,
distance: false,
direction: true,
offset: 1000,
},
},
initialize: function (tracks, options) {
L.Util.setOptions(this, options);
this._count = 0;
this._loadedCount = 0;
this._tracks = tracks;
this._layers = L.featureGroup();
this._markers = L.featureGroup();
this._hotline = L.featureGroup();
this._elevation = L.control.elevation(this.options.elevation_options);
this._routes = [];
this.options.points.forEach((poi) =>
L
.marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) })
.bindTooltip(poi.name, { direction: 'auto' }).addTo(this._markers)
);
},
getBounds: function () {
return this._layers.getBounds();
},
addTo: function (map) {
this._hotline.addTo(map);
this._layers.addTo(map);
this._markers.addTo(map);
this._map = map;
this.on('selection_changed', this._onSelectionChanged, this);
this.addTracks();
},
addTracks() {
this._tracks.forEach(this._addTrack, this);
},
highlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.eachLayer((l) => {
this.highlight(route, l)
})
},
unHighlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.eachLayer((l) => {
this.unhighlight(route, l)
})
},
select(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this.setSelection(route)
},
resetSelection() {
this.setSelection(this._selected)
if (this.options.flyToBounds) {
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
}
},
openPopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.openPopup();
},
closePopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.closePopup();
},
_addTrack: function (track) {
this._elevation._parseFromString(track.gpx)
.then(geojson => this._loadGeoJSON(geojson, track.id, track.gpx.split('/').pop().split('#')[0].split('?')[0]))
},
clear: function () {
this._elevation.clear()
this._elevation.remove();
this._clearLayers();
this._clearLayers(this._markers);
this._clearLayers(this._hotline)
this._count = 0;
this._loadedCount = 0;
this._tracks = []
this._routes = []
this.fire('clear')
},
_clearLayers(l) {
l = l || this._layers;
if (l && l.eachLayer) {
l.eachLayer(f => f.remove())
l.clearLayers();
}
},
_loadGeoJSON: function (geojson, id, fallbackName) {
if (geojson) {
geojson.id = id
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
this._loadRoute(geojson);
}
},
_loadRoute: function (data) {
if (!data) return;
var line_style = {
color: this._stringToColor(data.id),
opacity: 0.75,
weight: 5,
distanceMarkers: this.options.distanceMarkers_options,
};
var route = L.geoJson(data, {
name: data.name || '',
style: (feature) => line_style,
distanceMarkers: line_style.distanceMarkers,
originalStyle: line_style,
isGroupLayer: true,
id: data.id,
filter: feature => feature.geometry.type != "Point",
});
this._elevation.import([this._elevation.__LGEOMUTIL, this._elevation.__LDISTANCEM]).then(() => {
route.addTo(this._layers);
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
this._onEachRouteLoaded(route);
});
},
_onEachRouteLayer: function (route, layer) {
var polyline = layer;
this._routes.push(route)
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
polyline.on('mouseout', L.bind(this._onRouteMouseOut, this, route, polyline));
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
const startIcon = L.divIcon({
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-bullseye -translate-x-1/2"></i>',
className: 'start-icon'
});
const endIcon = L.divIcon({
html: '<i class="p-2 text-white bg-gray-500 rounded-full fa fa-flag-checkered -translate-x-1/2"></i>',
className: 'end-icon'
});
const latlngs = polyline.getLatLngs().flat(1);
polyline.setLatLngs(latlngs);
if (this._loadedCount == 0 || !this.options.itinerary) {
L.marker(latlngs[0], { icon: startIcon }).addTo(this._markers)
}
L.marker(latlngs[latlngs.length - 1], { icon: endIcon }).addTo(this._markers)
},
_onEachRouteLoaded: function (route) {
this.fire('route_loaded', { route: route });
if (++this._loadedCount === this._tracks.length) {
this.fire('loaded');
if (this.options.flyToBounds) {
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
}
}
},
highlight: function (route, polyline) {
polyline.setStyle(this.options.highlight);
polyline.options.highlighted = true
if (this.options.distanceMarkers) {
polyline.addDistanceMarkers();
}
},
unhighlight: function (route, polyline) {
polyline.setStyle(route.options.originalStyle);
polyline.options.highlighted = false
if (this.options.distanceMarkers) {
polyline.removeDistanceMarkers();
}
},
_onRouteMouseOver: function (route, polyline) {
if (!route.isSelected()) {
this.highlight(route, polyline);
}
this.fire('route_mouseover', { route: route, polyline: polyline });
},
_onRouteMouseOut: function (route, polyline) {
if (!route.isSelected()) {
this.unhighlight(route, polyline);
}
this.fire('route_mouseout', { route: route, polyline: polyline });
},
_onRouteClick: function (route, polyline) {
this.highlight(route, polyline)
this.setSelection(route);
},
_onRouteSelected: function (route, polyline) {
if (!route.isSelected()) {
this.unhighlight(route, polyline);
}
},
_onSelectionChanged: async function (e) {
var elevation = this._elevation;
var eleDiv = elevation.getContainer();
var route = this.getSelection();
var hotline = this._hotline;
hotline.clearLayers();
if (route && route.isSelected()) {
elevation.clear();
if (!eleDiv) {
elevation.addTo(this._map);
this._map.invalidateSize()
}
for (const layer of route.getLayers()) {
if (layer instanceof L.Polyline) {
await elevation.addData(layer, false);
}
}
await elevation._initHotLine(route, this._hotline)
this._map.flyToBounds(route.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
} else {
if (eleDiv) {
elevation.remove();
this._map.invalidateSize()
}
}
},
_stringToColor(input) {
// Define the maximum brightness to ensure the color is not too light
const maxBrightness = 200;
// Divide the string into 3 parts (5 characters each)
const redPart = input.slice(0, 5);
const greenPart = input.slice(5, 10);
const bluePart = input.slice(10, 15);
// Calculate the red, green, and blue values by summing the char codes of the parts
const red = Math.floor((redPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
const greenRaw = greenPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256;
const green = Math.floor((greenRaw * 0.5) * (maxBrightness / 255)); // Reduce green to avoid greenish colors
const blue = Math.floor((bluePart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
// Format as HEX color
return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`;
},
removeFrom: function (map) {
this._layers.removeFrom(map);
},
});
L.GpxGroup.include(L.Mixin.Events);
L.GpxGroup.include(L.Mixin.Selection);
L.gpxGroup = (tracks, options) => new L.GpxGroup(tracks, options);

File diff suppressed because it is too large Load Diff

View File

@@ -1,350 +0,0 @@
.leaflet-hidden {
visibility: hidden;
}
.legend {
cursor: pointer;
}
.leaflet-container {
z-index: 0;
/* prevent overlapping the .elevation-detached chart */
}
.elevation-control .background {
background-color: var(--ele-bg, rgba(70, 130, 180, 0.2));
border-radius: 5px;
overflow: visible;
display: block;
touch-action: none;
user-select: none;
max-width: 100%;
}
.elevation-control .grid,
.elevation-control .area>foreignObject,
.elevation-control .axis,
.elevation-control .tooltip,
.height-focus.line {
pointer-events: none;
}
.elevation-control .axis line,
.elevation-control .axis path {
stroke: var(--ele-axis, #2D1130);
stroke-width: 1;
fill: none;
}
.elevation-control .grid .tick line {
stroke: var(--ele-grid, #EEE);
stroke-width: 1px;
shape-rendering: crispEdges;
}
.elevation-control .grid path {
stroke-width: 0;
}
.elevation-control .axis text,
.elevation-control .legend text,
.elevation-control .point text {
fill: #fff;
font-weight: 700;
paint-order: stroke fill;
stroke: #000;
stroke-width: 2px
}
.elevation-control .legend .legend-switcher-symbol {
fill: rgb(135, 135, 135);
}
.elevation-control .y.axis text {
text-anchor: end;
}
.elevation-control .area {
fill: var(--ele-area, #4682B4);
stroke: var(--ele-stroke, #000);
stroke-width: 1.2;
paint-order: stroke fill;
}
.elevation-control .horizontal-drag-line {
cursor: row-resize;
stroke: transparent;
stroke-dasharray: 5;
stroke-width: 1.1;
}
.elevation-control .active .horizontal-drag-line {
stroke: #000;
}
.elevation-control .horizontal-drag-label {
fill: #000;
font-weight: 700;
paint-order: stroke;
stroke: #FFF;
stroke-width: 2px;
}
.elevation-control .ruler {
color: #000;
cursor: row-resize;
}
.elevation-control .mouse-focus-line {
stroke: #000;
stroke-width: 1;
}
.elevation-control .mouse-focus-label-rect {
fill: #000;
fill-opacity: 0.75;
stroke-width: 1;
stroke: #444;
}
.elevation-control .mouse-focus-label-text {
fill: #FFF;
font-size: 10px;
}
.elevation-control .brush .overlay {
cursor: unset;
}
.elevation-control .brush .selection {
fill: var(--ele-brush, rgba(23, 74, 117, 0.4));
stroke: none;
fill-opacity: unset;
}
.elevation-summary {
font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
margin: var(--ele-sum-margin, 0 auto);
text-shadow: var(--ele-sum-shadow, 1px 0 0 #FFF, -1px 0 0 #FFF, 0 1px 0 #FFF, 0 -1px 0 #FFF, 1px 1px #FFF, -1px -1px 0 #FFF, 1px -1px 0 #FFF, -1px 1px 0 #FFF);
}
.elevation-summary>span:not(:last-child):after {
content: var(--ele-sum-sep, '');
}
.multiline-summary>span {
display: block;
}
.multiline-summary .download {
float: right;
margin-top: -3em;
margin-right: 2em;
font-weight: bold;
font-size: 1.2em;
}
.elevation-summary .summaryvalue {
font-weight: bold;
}
.elevation-toggle-icon {
background-color: #fff;
right: 5px;
top: 5px;
height: var(--ele-toggle-size, 36px);
width: var(--ele-toggle-size, 36px);
cursor: pointer;
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.4);
border-radius: 5px;
display: inline-block;
position: var(--ele-toggle-pos, relative);
}
.elevation-toggle-icon:before {
content: '\2716';
display: var(--ele-close-btn, none);
color: #000;
width: 100%;
line-height: 20px;
text-align: center;
font-weight: bold;
font-size: 15px;
}
.leaflet-elevation-pane .height-focus,
.leaflet-overlay-pane .height-focus {
stroke: #000;
fill: var(--ele-circle, var(--ele-area, #FFF));
}
.leaflet-elevation-pane .height-focus.line,
.leaflet-overlay-pane .height-focus.line {
stroke-width: 2;
}
.leaflet-elevation-pane .height-focus-label,
.leaflet-overlay-pane .height-focus-label {
font-size: 12px;
font-weight: 600;
fill: #000;
paint-order: stroke;
stroke: #FFF;
stroke-width: 2px;
}
.elevation-waypoint-icon:before,
.elevation-position-icon:before {
content: "";
width: 100%;
height: 100%;
display: inline-block;
background: var(--ele-marker) no-repeat center center / contain;
}
.elevation-polyline {
stroke: var(--ele-poly, var(--ele-area, #000));
filter: drop-shadow(1px 1px 0 #FFF) drop-shadow(-1px -1px 0 #FFF) drop-shadow(1px -1px 0 #FFF) drop-shadow(-1px 1px 0 #FFF);
}
/* CHART STATES /////////////////////////////////////////////////// */
.elevation-detached {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
height: auto;
width: 100%;
position: relative;
z-index: 0;
}
.elevation-detached .area {
fill-opacity: var(--ele-alpha, 0.8);
}
.elevation-detached.elevation-collapsed .elevation-summary {
display: block;
}
.elevation-detached.elevation-collapsed .elevation-toggle-icon {
top: 5px;
right: 9px;
bottom: 5px;
margin: auto;
}
.elevation-control.elevation-collapsed>* {
display: none;
}
.elevation-control.elevation-collapsed>.elevation-toggle-icon {
display: inline-block;
}
.elevation-detached {
--ele-sum-margin: 12px 35px;
--ele-sum-shadow: none;
--ele-toggle-pos: absolute;
}
.elevation-expanded {
--ele-close-btn: inline-block;
--ele-toggle-bg: none;
--ele-toggle-pos: absolute;
--ele-toggle-size: 20px;
}
.inline-summary {
--ele-sum-sep: "\0020\2014\0020";
}
.elevation-waypoint-icon {
--ele-marker: url(../images/elevation-pushpin.svg);
}
.elevation-position-icon {
--ele-marker: url(../images/elevation-position.svg);
}
/* LIME THEME ///////////////////////////////////////////////////// */
.lime-theme {
--ele-bg: rgba(156, 194, 34, 0.2);
--ele-axis: #566B13;
--ele-area: #9CC222;
--ele-grid: #CCC;
--ele-brush: rgba(99, 126, 11, 0.4);
--ele-poly: #566B13;
--ele-line: #70ab00;
}
/* STEELBLUE THEME //////////////////////////////////////////////// */
.steelblue-theme {
--ele-axis: #0D1821;
--ele-area: #4682B4;
--ele-brush: rgba(23, 74, 117, 0.4);
--ele-line: #174A75;
}
/* PURPLE THEME /////////////////////////////////////////////////// */
.purple-theme {
--ele-bg: rgba(115, 44, 123, 0.2);
--ele-area: #732C7B;
--ele-brush: rgba(74, 14, 80, 0.4);
--ele-line: #732c7b;
}
/* YELLOW THEME /////////////////////////////////////////////////// */
.yellow-theme {
--ele-area: #FF0;
}
/* RED THEME ////////////////////////////////////////////////////// */
.red-theme {
--ele-area: #F00;
}
/* MAGENTA THEME ////////////////////////////////////////////////// */
.magenta-theme {
--ele-bg: rgba(255, 255, 255, 0.47);
--ele-area: #FF005E;
}
/* LIGHTBLUE THEME //////////////////////////////////////////////// */
.lightblue-theme {
--ele-area: #3366CC;
--ele-alpha: 0.45;
--ele-stroke: #4682B4;
--ele-circle: #fff;
--ele-line: #000;
--ele-grid: rgba(var(--input-border));
}
/* GRAY THEME //////////////////////////////////////////////// */
.gray-theme {
--ele-bg: #ffffff;
--ele-area: #242734;
--ele-alpha: 0.45;
--ele-stroke: #7b7b7b;
--ele-circle: #fff;
--ele-line: #000;
--ele-grid: #d1d5db;
}
.elevation-detached.lightblue-theme .area {
stroke: #3366CC;
}
/* leaflet-distance-markers */
.dist-marker {
font-size: 0.5rem;
border: 1px solid #777;
border-radius: 10px;
text-align: center;
color: #000;
background: #fff;
}
.leaflet-popup-content {
width: max-content !important;
max-width: 100%;
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (c) 2019, GPL-3.0+ Project, Raruto
*
* This file is free software: you may copy, redistribute and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) 2013-2016, MIT License, Felix “MrMufflon” Bache
*
* Permission to use, copy, modify, and/or distribute this software
* for any purpose with or without fee is hereby granted, provided
* that the above copyright notice and this permission notice appear
* in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
import * as _ from './utils';
import { Elevation } from './control';
Elevation.Utils = _;
L.control.elevation = (options) => new Elevation(options);

View File

@@ -1,90 +0,0 @@
import * as _ from './utils';
export var Options = {
autofitBounds: true,
autohide: false,
autohideMarker: true,
almostover: true,
altitude: true,
closeBtn: true,
collapsed: false,
detached: true,
distance: true,
distanceMarkers: { lazy: true, distance: true, direction: true },
dragging: !L.Browser.mobile,
downloadLink: 'link',
elevationDiv: "#elevation-div",
edgeScale: { bar: true, icon: false, coords: false },
followMarker: true,
imperial: false,
legend: true,
handlers: ["Distance", "Time", "Altitude", "Slope", "Speed", "Acceleration"],
hotline: 'elevation',
marker: 'elevation-line',
markerIcon: L.divIcon({
className: 'elevation-position-marker',
html: '<i class="elevation-position-icon"></i>',
iconSize: [32, 32],
iconAnchor: [16, 16],
}),
position: "topright",
polyline: {
className: 'elevation-polyline',
color: '#000',
opacity: 0.75,
weight: 5,
lineCap: 'round'
},
polylineSegments: {
className: 'elevation-polyline-segments',
color: '#F00',
interactive: false,
},
preferCanvas: false,
reverseCoords: false,
ruler: true,
theme: "lightblue-theme",
summary: 'inline',
slope: false,
speed: false,
time: true,
timeFactor: 3600,
timestamps: false,
showStartEnd: true,
mapTooltip: true,
trkStart: { className: 'start-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#00d800', fillOpacity: 1, interactive: false },
trkEnd: { className: 'end-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#ff0606', fillOpacity: 1, interactive: false },
waypoints: true,
wptIcons: {
'': L.divIcon({
className: 'elevation-waypoint-marker',
html: '<i class="elevation-waypoint-icon default"></i>',
iconSize: [30, 30],
iconAnchor: [8, 30],
}),
},
wptLabels: true,
xAttr: "dist",
xLabel: "km",
yAttr: "z",
yLabel: "m",
zFollow: false,
zooming: !L.Browser.Mobile,
// Quite uncommon and undocumented options
margins: { top: 30, right: 30, bottom: 30, left: 40 },
height: (screen.height * 0.3) || 200,
width: (screen.width * 0.6) || 600,
xTicks: undefined,
yTicks: undefined,
decimalsX: 2,
decimalsY: 0,
forceAxisBounds: false,
interpolation: "curveLinear",
yAxisMax: undefined,
yAxisMin: undefined,
// Prevent CORS issues for relative locations (dynamic import)
srcFolder: ((document.currentScript && document.currentScript.src) || (import.meta && import.meta.url)).split("/").slice(0,-1).join("/") + '/',
};

View File

@@ -1,214 +0,0 @@
/**
* TODO: exget computed styles of theese values from actual "CSS vars"
**/
export const Colors = {
'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
'magenta': { area: '#FF005E' },
'yellow': { area: '#FF0' },
'purple': { area: '#732C7B' },
'steelblue': { area: '#4682B4' },
'red': { area: '#F00' },
'lime': { area: '#9CC222', line: '#566B13' },
'gray': { area: '#000000', line: '#3366CC', alpha: 0.00001, stroke: '#000000' }
};
export const LineColors = [
'#0058ca',
'#E36414',
'5f0f40' ,
'#9A031E',
'#fb8b24',
];
const SEC = 1000;
const MIN = SEC * 60;
const HOUR = MIN * 60;
const DAY = HOUR * 24;
export function resolveURL(src, baseUrl) {
console.log(baseUrl, src);
return (new URL(src, (src.startsWith('../') || src.startsWith('./')) ? baseUrl : undefined)).toString()
};
/**
* Convert a time (millis) to a human readable duration string (%Dd %H:%M'%S")
*/
export function formatTime(t) {
let d = Math.floor(t / DAY);
let h = Math.floor((t - d * DAY) / HOUR);
let m = Math.floor((t - d * DAY - h * HOUR) / MIN);
let s = Math.round((t - d * DAY - h * HOUR - m * MIN) / SEC);
if (s === 60) { m++; s = 0; }
if (m === 60) { h++; m = 0; }
if (h === 24) { d++; h = 0; }
return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
}
/**
* Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
*/
export function formatDate(format) {
if (!format) {
return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
} else if (format == 'time') {
return (time) => (new Date(time)).toLocaleTimeString();
} else if (format == 'date') {
return (time) => (new Date(time)).toLocaleDateString();
}
return (time) => format(time);
}
/**
* Generate download data event.
*/
export function saveFile(dataURI, fileName) {
let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
let b = document.body;
b.appendChild(a);
a.click();
b.removeChild(a);
}
/**
* Convert SVG Path into Path2D and then update canvas
*/
export function drawCanvas(ctx, path) {
path.classed('canvas-path', true);
ctx.beginPath();
ctx.moveTo(0, 0);
let p = new Path2D(path.attr('d'));
ctx.strokeStyle = path.__strokeStyle || path.attr('stroke');
ctx.fillStyle = path.__fillStyle || path.attr('fill');
ctx.lineWidth = 1.25;
ctx.globalCompositeOperation = 'source-over';
// stroke opacity
ctx.globalAlpha = path.attr('stroke-opacity') || 0.3;
ctx.stroke(p);
// fill opacity
ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
ctx.fill(p);
ctx.globalAlpha = 1;
ctx.closePath();
}
/**
* Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
*/
export function coordPropsToMeta(coordProps, name, parser) {
return coordProps && (({ props, point, id, isMulti }) => {
if (props) {
for (const key of coordProps) {
if (key in props) {
point.meta[name] = (parser || parseNumeric).call(this, (isMulti ? props[key][isMulti] : props[key]), id);
break;
}
}
}
});
}
/**
* Extract numeric property (id) from GeoJSON object
*/
export const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
/**
* Extract datetime property (id) from GeoJSON object
*/
export const parseDate = (property, id) => new Date(Date.parse((typeof property === 'object' ? property[id] : property)));
/**
* A little bit shorter than L.DomUtil
*/
export const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
export const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
export const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
export const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
export const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
export const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
export const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
export const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
export const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
export const append = (n, c) => n.appendChild(c);
export const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
export const select = (str, n) => (n || document).querySelector(str);
export const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
export const randomId = () => Math.random().toString(36).substr(2, 9);
/**
* TODO: use generators instead? (ie. "yield")
*/
export const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
export const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
export const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
export const iSum = (iVal, sum = 0) => iVal + sum;
/**
* Alias for some leaflet core functions
*/
export const { on, off } = L.DomEvent;
export const { throttle, wrapNum } = L.Util;
export const { hasClass } = L.DomUtil;
/**
* Limit floating point precision
*/
export const round = L.Util.formatNum;
/**
* Limit a number between min / max values
*/
export const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
/**
* Limit a delta difference between two values
*/
export const wrapDelta = (curr, prev, deltaMax) => Math.abs(curr - prev) > deltaMax ? prev + deltaMax * Math.sign(curr - prev) : curr;
/**
* A deep copy implementation that takes care of correct prototype chain and cycles, references
*
* @see https://web.dev/structured-clone/#features-and-limitations
*/
export function cloneDeep(o, skipProps = [], cache = []) {
switch (!o || typeof o) {
case 'object':
const hit = cache.filter(c => o === c.original)[0];
if (hit) return hit.copy; // handle circular structures
const copy = Array.isArray(o) ? [] : Object.create(Object.getPrototypeOf(o));
cache.push({ original: o, copy });
Object
.getOwnPropertyNames(o)
.forEach(function (prop) {
const propdesc = Object.getOwnPropertyDescriptor(o, prop);
Object.defineProperty(
copy,
prop,
propdesc.get || propdesc.set
? propdesc // just copy accessor properties
: { // deep copy data properties
writable: propdesc.writable,
configurable: propdesc.configurable,
enumerable: propdesc.enumerable,
value: skipProps.includes(prop) ? propdesc.value : cloneDeep(propdesc.value, skipProps, cache),
}
);
});
return copy;
case 'function':
case 'symbol':
console.warn('cloneDeep: ' + typeof o + 's not fully supported:', o);
case true:
// null, undefined or falsy primitive
default:
return o;
}
}

View File

@@ -1,11 +0,0 @@
.leaflet-grid-label .gridlabel-vert {
margin-left: 8px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.leaflet-grid-label .gridlabel-vert,
.leaflet-grid-label .gridlabel-horiz {
padding-left:2px;
text-shadow: -2px 0 #FFFFFF, 0 2px #FFFFFF, 2px 0 #FFFFFF, 0 -2px #FFFFFF;
}

View File

@@ -1,233 +0,0 @@
import { LatLngBounds, type LatLngExpression, type LayerOptions, type LeafletEventHandlerFnMap, type PolylineOptions, Map, LayerGroup, Util, Polyline, LatLng, marker, divIcon, latLngBounds } from 'leaflet';
import './leaflet-auto-graticule.css';
export interface AutoGraticuleOptions extends LayerOptions {
redraw: keyof LeafletEventHandlerFnMap,
/** Minimum distance between two lines in pixels */
minDistance: number
}
export default class AutoGraticule extends LayerGroup {
options: AutoGraticuleOptions = {
redraw: 'moveend',
minDistance: 100 // Minimum distance between two lines in pixels
};
lineStyle: PolylineOptions = {
stroke: true,
color: '#333',
opacity: 0.6,
weight: 1,
interactive: false
};
_bounds!: LatLngBounds;
constructor(options?: Partial<AutoGraticuleOptions>) {
super();
Util.setOptions(this, options);
}
onAdd(map: Map) {
this._map = map;
this.redraw();
this._map.on('viewreset ' + this.options.redraw, this.redraw, this);
this.eachLayer(map.addLayer, map);
return this;
}
onRemove(map: Map) {
map.off('viewreset ' + this.options.redraw, this.redraw, this);
this.eachLayer(this.removeLayer, this);
return this;
}
redraw() {
this._bounds = this._map.getBounds().pad(0.5);
this.clearLayers();
this.constructLines();
return this;
}
constructLines() {
const bounds = this._map.getBounds();
const zoom = this._map.getZoom();
// Fix drawing of lines outside of bounds
this._bounds = AutoGraticule.bboxIntersect(bounds, [[-85, -180], [85, 180]]);
// Fix drawing of labels outside of bounds
const getBoundsBkp = this._map.getBounds;
try {
this._map.getBounds = function () {
return AutoGraticule.bboxIntersect(getBoundsBkp.apply(this), [[-85, -180], [85, 180]])
};
// Longitude: Draw vartical lines with a fixed distance between each other
const center = this._map.project(bounds.getCenter(), zoom);
const divisor = AutoGraticule.getGridDivisor(this._map.unproject(center.add([this.options.minDistance / 2, 0]), zoom).lng - this._map.unproject(center.subtract([this.options.minDistance / 2, 0]), zoom).lng, false);
const west = Math.max(bounds.getWest(), -180);
const east = Math.min(bounds.getEast(), 180);
for (let lng = AutoGraticule.fixFloatingPoint(Math.ceil(west / divisor) * divisor); lng <= east; lng += divisor) {
this.addLayer(this.buildXLine(lng));
this.addLayer(this.buildLabel('gridlabel-horiz', AutoGraticule.fixFloatingPoint(lng)));
}
// Latitude: Draw horizontal lines with a variable distance between each other (as in Mercator projection, the distance
// between latitudes gets bigger towards the poles). Calculate a divisor that all latitude line coordinates must be
// dividable by, but then only draw those coordinates that keep the minimum distance to their neighbour lines.
// Draw lines north of the equator separately from lines south of the equator (to keep the grid symmetical in
// relation to the equator).
if (bounds.getNorth() > 0) {
let lat = Math.max(0, bounds.getSouth());
let first = true;
while (lat < bounds.getNorth() && lat < 85) {
const point = this._map.project([lat, bounds.getCenter().lng], zoom);
const point2LatLng = this._map.unproject(point.subtract([0, this.options.minDistance]), zoom);
const divisor = AutoGraticule.getGridDivisor(point2LatLng.lat - lat, true);
lat = AutoGraticule.fixFloatingPoint(first ? Math.ceil(lat / divisor) * divisor : Math.ceil(point2LatLng.lat / divisor) * divisor);
first = false;
this.addLayer(this.buildYLine(lat));
this.addLayer(this.buildLabel('gridlabel-vert', lat));
}
}
if (bounds.getSouth() < 0) {
let lat = Math.min(0, bounds.getNorth());
let first = true;
while (lat > bounds.getSouth() && lat > -85) {
const point = this._map.project([lat, bounds.getCenter().lng], zoom);
const point2LatLng = this._map.unproject(point.add([0, this.options.minDistance]), zoom);
const divisor = AutoGraticule.getGridDivisor(AutoGraticule.fixFloatingPoint(lat - point2LatLng.lat), true);
lat = AutoGraticule.fixFloatingPoint(first ? Math.floor(lat / divisor) * divisor : Math.floor(point2LatLng.lat / divisor) * divisor);
first = false;
this.addLayer(this.buildYLine(lat));
this.addLayer(this.buildLabel('gridlabel-vert', lat));
}
}
} finally {
this._map.getBounds = getBoundsBkp;
}
}
buildXLine(x: number): Polyline {
const bottomLL = new LatLng(this._bounds.getSouth(), x);
const topLL = new LatLng(this._bounds.getNorth(), x);
return new Polyline([bottomLL, topLL], this.lineStyle);
}
buildYLine(y: number): L.Polyline {
const leftLL = new LatLng(y, this._bounds.getWest());
const rightLL = new LatLng(y, this._bounds.getEast());
return new Polyline([leftLL, rightLL], this.lineStyle);
}
buildLabel(axis: 'gridlabel-horiz' | 'gridlabel-vert', val: number) {
const bounds = this._map.getBounds().pad(-0.003);
let latLng: LatLng;
if (axis == 'gridlabel-horiz') {
latLng = new LatLng(bounds.getNorth(), val);
} else {
latLng = new LatLng(val, bounds.getWest());
}
return marker(latLng, {
interactive: false,
icon: divIcon({
iconSize: [0, 0],
className: 'leaflet-grid-label',
html: '<div class="' + axis + '">' + val + '&#8239;°</div>'
})
});
}
/**
* Rounds the given number to a fixed number of decimals in order to avoid floating point inaccuracies
* (for example to make 0.1 + 0.2 = 0.3 instead of 0.30000000000000004).
*/
static fixFloatingPoint(number: number): number {
return AutoGraticule.round(number, 12);
}
/**
* Rounds the given number to the given number of decimals.
*/
static round(number: number, digits: number) {
const fac = Math.pow(10, digits);
return Math.round(number * fac) / fac;
}
/**
* Given the distance between two coordinates, floors this distance to 90, 60, 45, 30 or
* 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, ...
* This will define the distance between two grid lines.
* @param variableDistance This should be true when the distance between the grid lines will be variable, that is for the latitude
* lines (because in Meractor projection the distance between two latitutes gets larger towards the poles, but the distance
* between two longitudes is constant throughout the globe).
* For constant distance lines (longitude), a line should be shown for every coordinate dividable by the result of this
* function. For example, if the result is 45, a line should be shown for longitudes -180, -135, -90, -45, 0, 45, 90, 135, 180.
* For variable distance lines (latitude), a line should be shown for coordinates dividable by a result of this function, but
* not for every single coordinate but only those that have a minimum distance towards their neighbour coordinate line. For
* variable distance lines, the maximum number returned by this function is 5, meaning at low zoom levels, all latitude lines
* are dividable by 5 (but not every latitude dividable by 5 will get a line).
*/
static getGridDivisor(number: number, variableDistance: boolean) {
if (number <= 0 || !isFinite(number))
throw new Error("Invalid number " + number);
else {
if (variableDistance && number >= 5)
return 5;
if (number <= 10) {
let fac = 1;
while (number > 1) { fac *= 10; number /= 10; }
while (number <= 0.1) { fac /= 10; number *= 10; }
// Dist is now some number between 0.1 and 1, so we can round it conveniently and then multiply it again by fac to get back to the original dist
if (number == 0.1)
return AutoGraticule.fixFloatingPoint(0.1 * fac);
else if (number <= 0.2)
return AutoGraticule.fixFloatingPoint(0.2 * fac);
else if (number <= 0.5)
return AutoGraticule.fixFloatingPoint(0.5 * fac);
else
return fac;
} else if (number <= 30)
return 30;
else if (number <= 45)
return 45;
else if (number <= 60)
return 60;
else
return 90;
}
}
// Backwards compatibility
static niceRound = AutoGraticule.getGridDivisor;
static bboxIntersect(bbox1: LatLngBounds | LatLngExpression[], bbox2: LatLngBounds | LatLngExpression[]) {
const bounds1 = bbox1 instanceof LatLngBounds ? bbox1 : latLngBounds(bbox1);
const bounds2 = bbox2 instanceof LatLngBounds ? bbox2 : latLngBounds(bbox2);
return latLngBounds([
[Math.max(bounds1.getSouth(), bounds2.getSouth()), Math.max(bounds1.getWest(), bounds2.getWest())],
[Math.min(bounds1.getNorth(), bounds2.getNorth()), Math.min(bounds1.getEast(), bounds2.getEast())]
]);
}
}

View File

@@ -1,280 +0,0 @@
import queue from "./queue";
var cacheBusterDate = +new Date();
// leaflet-image
export default function leafletImage(map, callback) {
var dimensions = map.getSize(),
layerQueue = new queue(1);
var canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
// dummy canvas image when loadTile get 404 error
// and layer don't have errorTileUrl
var dummycanvas = document.createElement('canvas');
dummycanvas.width = 1;
dummycanvas.height = 1;
var dummyctx = dummycanvas.getContext('2d');
dummyctx.fillStyle = 'rgba(0,0,0,0)';
dummyctx.fillRect(0, 0, 1, 1);
// layers are drawn in the same order as they are composed in the DOM:
// tiles, paths, and then markers
map.eachLayer(drawTileLayer);
map.eachLayer(drawEsriDynamicLayer);
if (map._pathRoot) {
layerQueue.defer(handlePathRoot, map._pathRoot);
} else if (map._panes) {
var firstCanvas = map._panes.overlayPane.getElementsByTagName('canvas').item(0);
if (firstCanvas) {
layerQueue.defer(handlePathRoot, firstCanvas);
}
}
map.eachLayer(drawMarkerLayer);
layerQueue.awaitAll(layersDone);
function drawTileLayer(l) {
if (l instanceof L.TileLayer) layerQueue.defer(handleTileLayer, l);
else if (l._heat) layerQueue.defer(handlePathRoot, l._canvas);
}
function drawMarkerLayer(l) {
if (l instanceof L.Marker && l.options.icon instanceof L.Icon) {
layerQueue.defer(handleMarkerLayer, l);
}
}
function drawEsriDynamicLayer(l) {
if (!L.esri) return;
if (l instanceof L.esri.DynamicMapLayer) {
layerQueue.defer(handleEsriDymamicLayer, l);
}
}
function done() {
callback(null, canvas);
}
function layersDone(err, layers) {
if (err) throw err;
layers.forEach(function (layer) {
if (layer && layer.canvas) {
ctx.drawImage(layer.canvas, 0, 0);
}
});
done();
}
function handleTileLayer(layer, callback) {
// `L.TileLayer.Canvas` was removed in leaflet 1.0
var isCanvasLayer = (L.TileLayer.Canvas && layer instanceof L.TileLayer.Canvas),
canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d'),
bounds = map.getPixelBounds(),
zoom = map.getZoom(),
tileSize = layer.options.tileSize;
if (zoom > layer.options.maxZoom ||
zoom < layer.options.minZoom
) {
return callback();
}
var tileBounds = L.bounds(
bounds.min.divideBy(tileSize)._floor(),
bounds.max.divideBy(tileSize)._floor()),
tiles = [],
j, i,
tileQueue = new queue(1);
for (j = tileBounds.min.y; j <= tileBounds.max.y; j++) {
for (i = tileBounds.min.x; i <= tileBounds.max.x; i++) {
tiles.push(new L.Point(i, j));
}
}
tiles.forEach(function (tilePoint) {
var originalTilePoint = tilePoint.clone();
if (layer._adjustTilePoint) {
layer._adjustTilePoint(tilePoint);
}
var tilePos = originalTilePoint
.scaleBy(new L.Point(tileSize, tileSize))
.subtract(bounds.min);
if (tilePoint.y >= 0) {
if (isCanvasLayer) {
var tile = layer._tiles[tilePoint.x + ':' + tilePoint.y];
tileQueue.defer(canvasTile, tile, tilePos, tileSize);
} else {
var url = addCacheString(layer.getTileUrl(tilePoint));
tileQueue.defer(loadTile, url, tilePos, tileSize);
}
}
});
tileQueue.awaitAll(tileQueueFinish);
function canvasTile(tile, tilePos, tileSize, callback) {
callback(null, {
img: tile,
pos: tilePos,
size: tileSize
});
}
function loadTile(url, tilePos, tileSize, callback) {
var im = new Image();
im.crossOrigin = '';
im.onload = function () {
callback(null, {
img: this,
pos: tilePos,
size: tileSize
});
};
im.onerror = function (e) {
// use canvas instead of errorTileUrl if errorTileUrl get 404
if (layer.options.errorTileUrl != '' && e.target.errorCheck === undefined) {
e.target.errorCheck = true;
e.target.src = layer.options.errorTileUrl;
} else {
callback(null, {
img: dummycanvas,
pos: tilePos,
size: tileSize
});
}
};
im.src = url;
}
function tileQueueFinish(err, data) {
data.forEach(drawTile);
callback(null, { canvas: canvas });
}
function drawTile(d) {
ctx.drawImage(d.img, Math.floor(d.pos.x), Math.floor(d.pos.y),
d.size, d.size);
}
}
function handlePathRoot(root, callback) {
var bounds = map.getPixelBounds(),
origin = map.getPixelOrigin(),
canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
var pos = L.DomUtil.getPosition(root).subtract(bounds.min).add(origin);
try {
ctx.drawImage(root, pos.x, pos.y, canvas.width - (pos.x * 2), canvas.height - (pos.y * 2));
callback(null, {
canvas: canvas
});
} catch (e) {
console.error('Element could not be drawn on canvas', root); // eslint-disable-line no-console
}
}
function handleMarkerLayer(marker, callback) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
pixelBounds = map.getPixelBounds(),
minPoint = new L.Point(pixelBounds.min.x, pixelBounds.min.y),
pixelPoint = map.project(marker.getLatLng()),
url = "/vendor/leaflet-image/marker.png",
im = new Image(),
options = marker.options.icon.options,
size = options.iconSize,
pos = pixelPoint.subtract(minPoint),
anchor = L.point(options.iconAnchor || size && { x: size[0], y: size[1] });
if (size instanceof L.Point) size = [size.x, size.y];
var x = Math.round(pos.x - size[0] + anchor.x),
y = Math.round(pos.y - anchor.y);
canvas.width = dimensions.x;
canvas.height = dimensions.y;
im.crossOrigin = '';
im.onload = function () {
if (marker._icon.classList.contains("leaflet-grid-label")) {
const label = marker._icon.children[0].textContent;
ctx.font = '600 10px "IBMPlexSans"';
ctx.fillStyle = "#000";
ctx.fillText(label, x + 4, y + 12);
} else {
const waypointName = marker.options.meta?.waypointName;
const icon = getComputedStyle(marker._icon.children[0], ':before').getPropertyValue('content').substring(1, 2) ?? "\uf111";
ctx.drawImage(this, x, y, size[0], size[1]);
ctx.font = '600 14px "Font Awesome 6 Free"';
ctx.fillStyle = "#ffffff";
const iconWidth = ctx.measureText(icon).width
ctx.fillText(icon, x + size[0] / 2 - iconWidth / 2, y + 24);
if (waypointName) {
ctx.fillStyle = "rgba(49, 49, 49, 0.5)"
ctx.font = '600 12px "IBMPlexSans"';
const textWidth = ctx.measureText(waypointName).width
ctx.roundRect(size[0] / 2 + x - (textWidth + 16) / 2, y - 18, textWidth + 16, 20, 5)
ctx.fill();
ctx.fillStyle = "#ffffff";
ctx.fillText(waypointName, size[0] / 2 + x - (textWidth + 16) / 2 + 8, y - 4);
}
}
callback(null, {
canvas: canvas
});
};
im.src = url;
}
function handleEsriDymamicLayer(dynamicLayer, callback) {
var canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
var im = new Image();
im.crossOrigin = '';
im.src = addCacheString(dynamicLayer._currentImage._image.src);
im.onload = function () {
ctx.drawImage(im, 0, 0);
callback(null, {
canvas: canvas
});
};
}
function addCacheString(url) {
// If it's a data URL we don't want to touch this.
if (isDataURL(url)) {
return url;
}
return url + ((url.match(/\?/)) ? '&' : '?') + 'cache=' + cacheBusterDate;
}
function isDataURL(url) {
var dataURLRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i;
return !!url.match(dataURLRegex);
}
};

View File

@@ -1,120 +0,0 @@
var noabort = {};
const slice = [].slice;
function Queue(size) {
this._size = size;
this._call =
this._error = null;
this._tasks = [];
this._data = [];
this._waiting =
this._active =
this._ended =
this._start = 0; // inside a synchronous task callback?
}
Queue.prototype = queue.prototype = {
constructor: Queue,
defer: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("defer after await");
if (this._error != null) return this;
var t = slice.call(arguments, 1);
t.push(callback);
++this._waiting, this._tasks.push(t);
poke(this);
return this;
},
abort: function() {
if (this._error == null) abort(this, new Error("abort"));
return this;
},
await: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("multiple await");
this._call = function(error, results) { callback.apply(null, [error].concat(results)); };
maybeNotify(this);
return this;
},
awaitAll: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("multiple await");
this._call = callback;
maybeNotify(this);
return this;
}
};
function poke(q) {
if (!q._start) {
try { start(q); } // let the current task complete
catch (e) {
if (q._tasks[q._ended + q._active - 1]) abort(q, e); // task errored synchronously
else if (!q._data) throw e; // await callback errored synchronously
}
}
}
function start(q) {
while (q._start = q._waiting && q._active < q._size) {
var i = q._ended + q._active,
t = q._tasks[i],
j = t.length - 1,
c = t[j];
t[j] = end(q, i);
--q._waiting, ++q._active;
t = c.apply(null, t);
if (!q._tasks[i]) continue; // task finished synchronously
q._tasks[i] = t || noabort;
}
}
function end(q, i) {
return function(e, r) {
if (!q._tasks[i]) return; // ignore multiple callbacks
--q._active, ++q._ended;
q._tasks[i] = null;
if (q._error != null) return; // ignore secondary errors
if (e != null) {
abort(q, e);
} else {
q._data[i] = r;
if (q._waiting) poke(q);
else maybeNotify(q);
}
};
}
function abort(q, e) {
var i = q._tasks.length, t;
q._error = e; // ignore active callbacks
q._data = undefined; // allow gc
q._waiting = NaN; // prevent starting
while (--i >= 0) {
if (t = q._tasks[i]) {
q._tasks[i] = null;
if (t.abort) {
try { t.abort(); }
catch (e) { /* ignore */ }
}
}
}
q._active = NaN; // allow notification
maybeNotify(q);
}
function maybeNotify(q) {
if (!q._active && q._call) {
var d = q._data;
q._data = undefined; // allow gc
q._call(q._error, d);
}
}
export default function queue(concurrency) {
if (concurrency == null) concurrency = Infinity;
else if (!((concurrency = +concurrency) >= 1)) throw new Error("invalid concurrency");
return new Queue(concurrency);
}

View File

@@ -0,0 +1,42 @@
import * as M from "maplibre-gl";
import { type ControlPosition, type IControl } from "maplibre-gl";
export class FullscreenControl implements IControl {
private buttonContainer?: HTMLDivElement;
private fullscreenButton?: HTMLButtonElement;
private iconSpan?: HTMLSpanElement;
private callback: (e: MouseEvent) => void;
constructor(callback: (e: MouseEvent) => void) {
this.callback = callback;
}
onAdd(map: M.Map): HTMLElement {
this.buttonContainer = document.createElement("div");
this.buttonContainer.classList.add(
"maplibregl-ctrl",
"maplibregl-ctrl-group"
);
this.fullscreenButton = document.createElement("button");
this.buttonContainer.appendChild(this.fullscreenButton);
this.iconSpan = document.createElement("i");
this.iconSpan.classList.add("fa", "fa-maximize", "text-black");
this.fullscreenButton.appendChild(this.iconSpan);
this.fullscreenButton.addEventListener("click", this.callback);
return this.buttonContainer;
}
onRemove(): void {
// remove button
if (this.buttonContainer?.parentNode) {
this.buttonContainer.parentNode.removeChild(this.buttonContainer);
}
this.buttonContainer = undefined;
this.fullscreenButton = undefined;
}
}

View File

@@ -2,9 +2,6 @@
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte"; import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte"; import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import { trail } from "$lib/stores/trail_store"; import { trail } from "$lib/stores/trail_store";
import "$lib/vendor/leaflet-elevation/src/index.css";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
import "photoswipe/style.css"; import "photoswipe/style.css";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";

View File

@@ -15,16 +15,11 @@
formatElevation, formatElevation,
formatTimeHHMM, formatTimeHHMM,
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import { import { calculatePixelPerMeter, calculateScaleFactor } from "$lib/util/maplibre_util";
calculatePixelPerMeter,
calculateScaleFactor,
} from "$lib/util/leaflet_util";
import { createRect, createText } from "$lib/util/svg_util"; import { createRect, createText } from "$lib/util/svg_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index"; import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
import { jsPDF } from "jspdf"; import { jsPDF } from "jspdf";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import * as M from "maplibre-gl"; import * as M from "maplibre-gl";
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";

File diff suppressed because one or more lines are too long

View File

@@ -1,314 +0,0 @@
/*
* Copyright (c) 2022, GPL-3.0+ Project, Raruto
*
* This file is free software: you may copy, redistribute and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) 2014- Doroszlai Attila, 2016- Phil Whitehurst
*
* Permission to use, copy, modify, and/or distribute this software
* for any purpose with or without fee is hereby granted, provided
* that the above copyright notice and this permission notice appear
* in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// TODO: the "L.DistanceMarker" (canvas marker) class could be alternatively provided by "leaflet-rotate"?
L.DistanceMarker = L.CircleMarker.extend({
_updatePath: function () {
let ctx = this._renderer._ctx;
let p = this._point;
// Calculate image direction (rotation)
this.options.rotation = this.options.rotation || 0;
// Draw circle marker (canvas point)
if (this.options.radius && this._renderer._updateCircle) {
this._renderer._updateCircle(this);
}
// Draw image over circle (distance marker)
if (this.options.icon && this.options.icon.url) {
if (!this.options.icon.element) {
const icon = document.createElement('img');
this.options.icon = L.extend({ rotate: 0, size: [40, 40], offset: { x: 0, y: 0 } }, this.options.icon);
this.options.icon.rotate += this.options.rotation;
this.options.icon.element = icon;
icon.src = this.options.icon.url;
icon.onload = () => this.redraw();
icon.onerror = () => this.options.icon = null;
} else {
const icon = this.options.icon;
let cx = p.x + icon.offset.x;
let cy = p.y + icon.offset.y;
ctx.save();
if (icon.rotate) {
ctx.translate(p.x, p.y);
ctx.rotate(icon.rotate);
cx = 0;
cy = 0;
}
ctx.drawImage(icon.element, cx - icon.size[0] / 2, cy - icon.size[1] / 2, icon.size[0], icon.size[1]);
ctx.restore();
}
}
// Add a label inside the circle (distance marker)
if (this.options.label) {
let cx = p.x, cy = p.y;
ctx.save();
ctx.font = this.options.font || 'normal 7pt "Helvetica Neue", Arial, Helvetica, sans-serif';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = this.options.fillStyle || 'black';
// TODO rescale circle to fit text
// let fontSize = Number(/[0-9\.]+/.exec(ctx.font)[0]);
// let fontWidth = ctx.measureText(this.options.html).width;
if (this.options.rotation) {
ctx.translate(p.x, p.y);
ctx.rotate(this.options.rotation);
cx = 0;
cy = 0;
}
// Temporary fix to prevent stroke blurs at higher zoom levels
if (this._map.getZoom() > 17) {
ctx.fillStyle = this.options.strokeStyle || 'black';
}
ctx.fillText(this.options.label, cx, cy);
if (this.options.strokeStyle && this._map.getZoom() <= 17) {
ctx.strokeStyle = this.options.strokeStyle;
ctx.strokeText(this.options.label, cx, cy);
}
ctx.restore();
}
}
});
L.DistanceMarkers = L.LayerGroup.extend({
options: {
cssClass: 'dist-marker',
iconSize: [12, 12],
arrowSize: [10, 10],
arrowUrl: "data:image/svg+xml,%3Csvg transform='rotate(90)' xmlns='http://www.w3.org/2000/svg' width='560px' height='560px' viewBox='0 0 560 560'%3E%3Cpath stroke-width='35' fill='%23000' stroke='%23FFF' d='M280,40L522,525L280,420L38,525z'/%3E%3C/svg%3E",
offset: 1000,
showAll: 12,
textFunction: (distance, i, offset) => i,
distance: true,
direction: true,
},
initialize: function (line, map, options) {
this._layers = {};
this._zoomLayers = {};
options = L.setOptions(this, options);
let preferCanvas = map.options.preferCanvas;
let showAll = Math.min(map.getMaxZoom(), options.showAll);
// You should use "leaflet-rotate" to show rotated arrow markers (preferCanvas: false)
if (!preferCanvas && !map.options.rotate) {
console.warn('Missing dependency: "leaflet-rotate"');
}
// Get line coords as an array
let coords = typeof line.getLatLngs == 'function' ? line.getLatLngs() : line;
// Handle "MultiLineString" features
coords = L.LineUtil.isFlat(coords) ? [coords] : coords;
coords.forEach(latlngs => {
// Get accumulated line lengths as well as overall length
let accumulated = L.GeometryUtil.accumulatedLengths(latlngs);
let length = accumulated.length > 0 ? accumulated[accumulated.length - 1] : 0;
// count = Number of distance markers to be added
// j = Position in accumulated line length array
for (let i = 1, count = Math.floor(length / options.offset), j = 0; i <= count; ++i) {
let distance = options.offset * i;
// Find the first accumulated distance that is greater than the distance of this marker
while (j < accumulated.length - 1 && accumulated[j] < distance) ++j;
// Grab two nearest points either side marker position
let p1 = latlngs[j - 1];
let p2 = latlngs[j];
let m_line = L.polyline([p1, p2]);
// and create a simple line to interpolate on
let ratio = (distance - accumulated[j - 1]) / (accumulated[j] - accumulated[j - 1]);
let position = L.GeometryUtil.interpolateOnLine(map, m_line, ratio);
let delta = map.project(p2).subtract(map.project(p1));
let angle = Math.atan2(delta.y, delta.x);
// Generate distance marker label
let text = options.textFunction.call(this, distance, i, options.offset);
// Grouping layer of visible layers at zoom level (arrow + distance)
let zoom = this._minimumZoomLevelForItem(i, showAll);
let markers = this._zoomLayers[zoom] = this._zoomLayers[zoom] || L.layerGroup()
// create arrow markers
if (options.direction && ((options.distance && i % 2 == 1) || !options.distance)) {
if (preferCanvas) {
markers.addLayer(
new L.DistanceMarker(p1, {
radius: 0,
icon: {
url: options.arrowUrl, //image link
size: options.arrowSize, //image size ( default [40, 40] )
rotate: 0, //image base rotate ( default 0 )
offset: { x: 0, y: 0 }, //image offset ( default { x: 0, y: 0 } )
},
rotation: angle,
interactive: false,
// label: '⮞', //'➜',
// font: 'normal 20pt "Helvetica Neue", Arial, Helvetica, sans-serif',
// fillStyle: 'white',//'#3366CC',
// strokeStyle: 'black',
})
);
} else {
markers.addLayer(
L.marker(position.latLng, {
icon: L.icon({
iconUrl: options.arrowUrl,
iconSize: options.arrowSize,
}),
// NB the following option is added by "leaflet-rotate"
rotation: angle,
interactive: false,
})
);
}
}
// create distance markers
if (options.distance && i % 2 == 0) {
if (preferCanvas) {
markers.addLayer(
new L.DistanceMarker(position.latLng, {
label: text, // TODO: handle text rotation (leaflet-rotate)
radius: 7,
fillColor: '#fff',
fillOpacity: 1,
fillStyle: 'black',
color: '#777',
weight: 1,
interactive: false,
})
);
} else {
markers.addLayer(
L.marker(position.latLng, {
title: text,
icon: L.divIcon({
className: options.cssClass,
html: text,
iconSize: options.iconSize
}),
interactive: false,
})
);
}
}
}
});
const updateMarkerVisibility = () => {
let oldZoom = this._lastZoomLevel || 0;
let newZoom = map.getZoom();
if (newZoom > oldZoom) {
for (let i = oldZoom + 1; i <= newZoom; ++i) {
if (this._zoomLayers[i] !== undefined) {
this.addLayer(this._zoomLayers[i]);
}
}
} else if (newZoom < oldZoom) {
for (let i = oldZoom; i > newZoom; --i) {
if (this._zoomLayers[i] !== undefined) {
this.removeLayer(this._zoomLayers[i]);
}
}
}
this._lastZoomLevel = newZoom;
};
map.on('zoomend', updateMarkerVisibility);
updateMarkerVisibility();
},
_minimumZoomLevelForItem: function (i, zoom) {
while (i > 0 && i % 2 === 0) {
--zoom;
i = Math.floor(i / 2);
}
return zoom;
},
});
L.Polyline.include({
_originalOnAdd: L.Polyline.prototype.onAdd,
_originalOnRemove: L.Polyline.prototype.onRemove,
addDistanceMarkers: function () {
if (this._map && this._distanceMarkers) {
this._map.addLayer(this._distanceMarkers);
}
},
removeDistanceMarkers: function () {
if (this._map && this._distanceMarkers) {
this._map.removeLayer(this._distanceMarkers);
}
},
onAdd: function (map) {
this._originalOnAdd(map);
let opts = this.options.distanceMarkers || {};
if (this.options.distanceMarkers) {
this._distanceMarkers = this._distanceMarkers || new L.DistanceMarkers(this, map, opts);
}
if (opts.lazy === undefined || opts.lazy === false) {
this.addDistanceMarkers();
}
},
onRemove: function (map) {
this.removeDistanceMarkers();
this._originalOnRemove(map);
}
});

File diff suppressed because one or more lines are too long

View File

@@ -1,636 +0,0 @@
/*
* Copyright (c) 2023, GPL-3.0+ Project, Raruto
*
* This file is free software: you may copy, redistribute and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) GPL-3.0+ Project - 2018- Dražen Tutić - https://github.com/dtutic/Leaflet.EdgeScaleBar
* Copyright (c) MIT License (MIT) - 2015- Xisco Guaita - https://github.com/xguaita/Leaflet.MapCenterCoord
*/
/**
* Original source: https://github.com/xguaita/Leaflet.MapCenterCoord
*/
L.Control.EdgeScale = L.Control.extend({
// Defaults
options: {
position: 'bottomleft',
icon: true,
coords: true,
bar: true,
onMove: true,
template: '{y} | {x}', // https://en.wikipedia.org/wiki/ISO_6709
projected: false,
formatProjected: '#.##0,000',
latlngFormat: 'DD', // DD, DM, DMS
latlngDesignators: true,
latLngFormatter: undefined,
iconStyle: {
background: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 100 100'%3E%3Cg stroke='%23fff'%3E%3Ccircle cx='50' cy='50.2' r='3.9' stroke-width='2' /%3E%3Cpath stroke-width='3' d='M5 54h32a4 4 0 1 0 0-8H5a4 4 0 1 0 0 8z M54 5a4 4 0 1 0-8 0v32a4 4 0 1 0 8 0V5z M99 50c0-2-2-4-4-4H63a4 4 0 1 0 0 8h32c2 0 4-1 4-4zM46 95a4 4 0 1 0 8 0V64a4 4 0 1 0-8 0v31z'/%3E%3C/g%3E%3C/svg%3E%0A")`,
width: '24px',
height: '24px',
left: 'calc(50% - 12px)',
top: 'calc(50% - 12px)',
content: '',
display: 'block',
position: 'absolute',
zIndex: 999,
pointerEvents: 'none',
},
containerStyle: {
backgroundColor: 'rgba(255, 255, 255, 0.7)',
boxShadow: '0 0 5px #bbb',
borderRadius: '3px',
padding: '3px 2px',
color: '#333',
font: '11px/1.5 Consolas, monaco, monospace',
writingMode: 'vertical-lr',
},
},
initialize: function(options) {
L.setOptions(this, options);
},
onAdd: function (map) {
if (this.options.bar) {
this._scaleBar = (new L.Control.EdgeScale.Layer(true === this.options.bar ? {} : this.options.bar)).addTo(map);
}
// create a DOM element and put it into overlayPane
if (this.options.icon) {
this._icon = L.DomUtil.create('div', 'leaflet-crosshair');
Object.assign(this._icon.style, this.options.iconStyle);
map.getContainer().insertBefore(this._icon, map.getContainer().firstChild);
}
// Control container
this._container = L.DomUtil.create('div', 'leaflet-control-mapcentercoord');
Object.assign(this._container.style, this.options.containerStyle);
if (!this.options.coords) {
this._container.style.display = 'none';
}
L.DomEvent.disableClickPropagation(this._container);
this._container.innerHTML = this._getMapCenterCoord();
// Add events listeners for updating coordinates & icon's position
map.on('move', this._onMapMove, this);
map.on('moveend', this._onMapMove, this);
return this._container;
},
onRemove: function (map) {
if (this.options.bar) {
this._scaleBar.remove();
}
// remove icon's DOM elements and listeners
if (this.options.icon) {
map.getContainer().removeChild(this._icon);
}
map.off('move', this._onMapMove, this);
map.off('moveend', this._onMapMove, this);
},
// update coordinates
_onMapMove: function (e) {
if (this.options.onMove || 'moveend' === e.type) {
this._container.innerHTML = this._getMapCenterCoord();
}
},
_getMapCenterCoord: function () {
const center = this._map.getCenter();
return this.options.projected
? this._getProjectedCoord(this._map.options.crs.project(center))
: this._getLatLngCoord(center);
},
_getProjectedCoord: function (center) {
return L.Util.template(
this.options.template,
{
x: this._format(this.options.formatProjected, center.x),
y: this._format(this.options.formatProjected, center.y)
}
);
},
_getLatLngCoord: function (latLng) {
const { latLngFormatter, latlngFormat, latlngDesignators: designators } = this.options;
if (undefined !== latLngFormatter ) {
return latLngFormatter(latLng.lat, latLng.lng);
}
let lat, lng, deg, min;
// make a copy of center so we aren't affecting leaflet's internal state
let center = {
lat: latLng.lat,
lng: latLng.lng,
lng_neg: latLng.lng < 0,
lat_neg: latLng.lat < 0,
};
// 180 degrees & negative
if (center.lng < 0) {
center.lng = Math.abs(center.lng);
}
if (center.lng > 180) {
center.lng = 360 - center.lng;
center.lng_neg = !center.lng_neg;
}
if (center.lat < 0) {
center.lat = Math.abs(center.lat);
}
// format
if ('DM' === latlngFormat) {
deg = parseInt(center.lng);
lng = deg + 'º ' + this._format('00.000', (center.lng - deg) * 60) + "'";
deg = parseInt(center.lat);
lat = deg + 'º ' + this._format('00.000', (center.lat - deg) * 60) + "'";
} else if ('DMS' === latlngFormat) {
deg = parseInt(center.lng);
min = (center.lng - deg) * 60;
lng = deg + 'º ' + this._format('00', parseInt(min)) + "' " + this._format('00.0', (min - parseInt(min)) * 60) + "''";
deg = parseInt(center.lat);
min = (center.lat - deg) * 60;
lat = deg + 'º ' + this._format('00', parseInt(min)) + "' " + this._format('00.0', (min - parseInt(min)) * 60) + "''";
} else { // 'DD'
lng = this._format('#0.00000', center.lng) + 'º';
lat = this._format('##0.00000', center.lat) + 'º';
}
return L.Util.template(this.options.template, {
x: (!designators && center.lng_neg ? '-' : '') + lng + (designators ? (center.lng_neg ? ' W' : ' E') : ''),
y: (!designators && center.lat_neg ? '-' : '') + lat + (designators ? (center.lat_neg ? ' S' : ' N') : '')
});
},
/**
* IntegraXor Web SCADA - JavaScript Number Formatter
*
* @see https://code.google.com/p/javascript-number-formatter
* @authors KPL, KHL
*/
_format: function (m, v) {
if (!m || isNaN(+v)) {
return v; // return as it is.
}
v = m.charAt(0) == '-' ? -v : +v; // convert any string to number according to formation sign.
let isNegative = v < 0 ? v = -v : 0; // process only abs(), and turn on flag.
let result = m.match(/[^\d\-\+#]/g); // search for separator for grp & decimal, anything not digit, not +/- sign, not #.
let Decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
let Group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
m = m.split(Decimal); // split the decimal for the format string if any.
v = v.toFixed(m[1] && m[1].length); // Fix the decimal first, toFixed will auto fill trailing zero.
v = +(v) + ''; // convert number to string to trim off *all* trailing decimal zero(es)
let pos_trail_zero = m[1] && m[1].lastIndexOf('0'); // fill back any trailing zero according to format (look for last zero in format)
let part = v.split('.');
if (!part[1] || part[1] && part[1].length <= pos_trail_zero) { // integer will get !part[1]
v = (+v).toFixed(pos_trail_zero + 1);
}
let szSep = m[0].split(Group); // look for separator
m[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
let pos_lead_zero = m[0] && m[0].indexOf('0');
if (pos_lead_zero > -1) {
while (part[0].length < (m[0].length - pos_lead_zero)) {
part[0] = '0' + part[0];
}
} else if (+part[0] == 0) {
part[0] = '';
}
v = v.split('.');
v[0] = part[0];
var pos_separator = (szSep[1] && szSep[szSep.length - 1].length); // process the first group separator from decimal (.) only, the rest ignore. Get the length of the last slice of split result.
if (pos_separator) {
let integer = v[0];
let str = '';
let offset = integer.length % pos_separator;
for (let i = 0, l = integer.length; i < l; i++) {
str += integer.charAt(i); // ie6 only support charAt for sz.
if (
!((i - offset + 1) % pos_separator) &&
i < l - pos_separator // -pos_separator so that won't trail separator on full length
) {
str += Group;
}
}
v[0] = str;
}
v[1] = (m[1] && v[1]) ? Decimal + v[1] : "";
return (isNegative ? '-' : '') + v[0] + v[1]; // put back any negation and combine integer and fraction.
}
});
/**
* Original Source: https://github.com/dtutic/Leaflet.EdgeScaleBar
*
* Draws the metric scale bars in Web Mercator map along top and right edges.
* Authors: Dražen Tutić (dtutic@geof.hr), Ana Kuveždić Divjak (akuvezdic@geof.hr)
* University of Zagreb, Faculty of Geodesy, GEOF-OSGL Lab
* Inspired by LatLonGraticule Leaflet plugin by: lanwei@cloudybay.com.tw
*/
L.Control.EdgeScale.Layer = L.Layer.extend({
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
options: {
opacity: 1,
weight: 0.8,
gradient: {
size: 10,
opacity: 0.5,
},
color: '#000',
font: '11px Arial',
zoomInterval: [
{start: 0, end: 2, interval: 5000000},
{start: 3, end: 3, interval: 2000000},
{start: 4, end: 4, interval: 1000000},
{start: 5, end: 5, interval: 500000},
{start: 6, end: 7, interval: 200000},
{start: 8, end: 8, interval: 100000},
{start: 9, end: 9, interval: 50000},
{start: 10, end: 10, interval: 20000},
{start: 11, end: 11, interval: 10000},
{start: 12, end: 12, interval: 5000},
{start: 13, end: 13, interval: 2000},
{start: 14, end: 14, interval: 1000},
{start: 15, end: 15, interval: 500},
{start: 16, end: 16, interval: 200},
{start: 17, end: 17, interval: 100},
{start: 18, end: 18, interval: 50},
{start: 19, end: 19, interval: 20},
{start: 20, end: 20, interval: 10}
],
pane: 'edgescalePane'
},
initialize: function (options) {
L.setOptions(this, options);
// Constants of the WGS84 ellipsoid needed to calculate meridian length or latitute
const a = this._a = 6378137.0;
const b = this._b = 6356752.3142;
const n = this._n = (a - b)/(a + b);
const a2 = a * a;
const b2 = b * b;
const n2 = n * n;
const n3 = n2 * n;
const n4 = n3 * n;
const n5 = n4 * n;
this._A = a * (1.0 - n) * (1.0 - n2) * (1.0 + 9.0/4.0 * n2 + 225.0/64.0 * n4);
this._e2 = (a2 - b2) / a2;
this._ic1 = 1.5 * n - 29.0/12.0 * n3 + 553.0/80.0 * n5;
this._ic2 = 21.0/8.0 * n2 - 1537.0/128.0 * n4;
this._ic3 = 151.0/24.0 * n3 - 32373.0/640.0 * n5;
this._ic4 = 1097.0/64.0 * n4;
this._ic5 = 8011.0/150.0 * n5;
this._c1 = -1.5 * n + 31.0/24.0 * n3 - 669.0/640.0 * n5;
this._c2 = 15.0/18.0 * n2 - 435.0/128.0 * n4;
this._c3 = -35.0/12.0 * n3 + 651.0/80.0 * n5;
this._c4 = 315.0/64.0 * n4;
this._c5 = -693.0/80.0 * n5;
// Latitude limit of the Web Mercator projection
this._LIMIT_PHI = 1.484419982;
},
onAdd: function (map) {
this._map = map;
let pane = map.getPane(this.options.pane);
if (!pane) {
pane = this._pane = map.createPane('edgescalePane', map.getPane('norotatePane') || map.getPane('mapPane'));
pane.style.zIndex = 625; // This pane is above markers but below popups.
pane.style.pointerEvents = 'none';
}
this._pane = pane;
// if (this._renderer) this._renderer.remove()
// this._renderer = L.canvas({ pane: "edgescalePane" }).addTo(this._map); // default leaflet svg renderer
if (!this._canvas) {
this._initCanvas();
}
this._pane.appendChild(this._canvas);
map.on('viewreset', this._reset, this);
map.on('move', this._reset, this);
map.on('moveend', this._reset, this);
map.on('rotate', this._reset, this);
this._reset();
},
onRemove: function (map) {
this._pane.removeChild(this._canvas);
map.off('viewreset', this._reset, this);
map.off('move', this._reset, this);
map.off('moveend', this._reset, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
L.DomUtil.setOpacity(this._canvas, this.options.opacity);
return this;
},
bringToFront: function () {
if (this._canvas) {
this._pane.appendChild(this._canvas);
}
return this;
},
bringToBack: function () {
if (this._canvas) {
this._pane.insertBefore(this._canvas, pane.firstChild);
}
return this;
},
_initCanvas: function () {
this._canvas = L.DomUtil.create('canvas', '');
this._ctx = this._canvas.getContext('2d');
this.setOpacity();
L.extend(this._canvas, {
onselectstart: L.Util.falseFn,
onmousemove: L.Util.falseFn,
onload: L.bind(this._onCanvasLoad, this)
});
},
_reset: function () {
var canvas = this._canvas,
size = this._map.getSize();
this._setCanvasPosition();
canvas.width = size.x;
canvas.height = size.y;
canvas.style.width = size.x + 'px';
canvas.style.height = size.y + 'px';
/**
* @TODO add support for "leaflet-rotate"
*/
if (this._map._bearing) {
return;
}
const { gradient } = this.options;
// horizontal gradient
if (!this._hor_gradient) {
this._hor_gradient = this._ctx.createLinearGradient(0, 0, 0, gradient.size);
this._hor_gradient.addColorStop(0,"rgba(255, 255, 255, " + gradient.opacity + ")");
this._hor_gradient.addColorStop(1,"rgba(255, 255, 255, 0)");
}
this._ctx.fillStyle = this._hor_gradient;
this._ctx.fillRect(0, 0, size.x, gradient.size);
// vertical gradient
if (!this._vert_gradient) {
this._vert_gradient = this._ctx.createLinearGradient(0, 0, gradient.size, 0);
this._vert_gradient.addColorStop(0,"rgba(255, 255, 255, " + gradient.opacity + ")");
this._vert_gradient.addColorStop(1,"rgba(255, 255, 255, 0)");
}
this._ctx.fillStyle = this._vert_gradient;
this._ctx.fillRect(0, 0, gradient.size, size.y);
this._ctx.beginPath();
this._ctx.moveTo(0,0);
this._ctx.lineTo(size.x,0);
this._ctx.lineTo(size.x,size.y);
this._ctx.stroke();
this._calcInterval();
this._draw();
},
_onCanvasLoad: function () {
this.fire('load');
},
_calcInterval: function() {
const { zoomInterval } = this.options;
const zoom = this._map.getZoom();
if (undefined !== zoomInterval) {
// Manually set scale using a custom this.options.zoomInterval object
for (const idx in zoomInterval) {
const dict = zoomInterval[idx];
if (dict.start <= zoom && dict.end && dict.end >= zoom) {
this._interval = dict.interval;
break;
}
}
} else {
// Autamatically get current scale using L.Control.Scale
// Source: https://gis.stackexchange.com/a/198444
this._interval = L.Control.Scale.prototype._getRoundNum(
this._map
.containerPointToLatLng([0, this._map.getSize().y / 2 ])
.distanceTo(
this._map.containerPointToLatLng([L.Control.Scale.prototype.options.maxWidth, this._map.getSize().y / 2 ]
)
)
);
}
this._currZoom = zoom;
},
_draw: function() {
this._ctx.strokeStyle = this.options.color;
this._create_lat_ticks();
this._create_lon_ticks();
this._ctx.fillStyle = this.options.color;
this._ctx.font = this.options.font;
const size = this._map.getSize();
const text = this._interval >= 1000 ? (this._interval / 1000 + ' km') : this._interval + ' m';
this._ctx.textAlign = 'left';
this._ctx.textBaseline = 'middle';
this._ctx.fillText(text, +12, size.y / 2);
this._ctx.textAlign = 'center';
this._ctx.textBaseline = 'top';
this._ctx.fillText(text, size.x / 2, 12);
},
_create_lat_ticks: function() {
const { weight } = this.options;
const size = this._map.getSize();
const to_rad = Math.PI/180.0;
const center = this._merLength(this._map.containerPointToLatLng(L.point(0, size.y / 2)).lat * to_rad);
const top = this._merLength(this._map.containerPointToLatLng(L.point(0,0)).lat * to_rad);
const bottom = this._merLength(this._map.containerPointToLatLng(L.point(0, size.y)).lat * to_rad);
// draw major ticks
for (let i = center + this._interval / 2; i < top; i = i + this._interval) {
const phi = this._invmerLength(i);
if ((phi < this._LIMIT_PHI) && (phi > -this._LIMIT_PHI)) {
this._draw_lat_tick(phi, 10, weight * 1.5);
}
}
for (let i = center - this._interval / 2; i > bottom; i = i - this._interval) {
const phi = this._invmerLength(i);
if ((phi > -this._LIMIT_PHI) && (phi < this._LIMIT_PHI)) {
this._draw_lat_tick(phi, 10, weight * 1.5);
}
}
// draw minor ticks
for (let i = center; i < top; i = i + this._interval / 10.0) {
const phi = this._invmerLength(i);
if ((phi < this._LIMIT_PHI) && (phi > -this._LIMIT_PHI)) {
this._draw_lat_tick(phi, 4, weight);
}
}
for (let i = center - this._interval / 10; i > bottom; i = i - this._interval / 10.0) {
const phi = this._invmerLength(i);
if ((phi > -this._LIMIT_PHI) && (phi < this._LIMIT_PHI)) {
this._draw_lat_tick(phi, 4, weight);
}
}
},
_create_lon_ticks: function() {
const { weight } = this.options;
const size = this._map.getSize();
const to_rad = Math.PI/180.0;
const to_deg = 180.0/Math.PI;
const center = this._map.containerPointToLatLng(L.point(size.x / 2, 0));
const left = this._map.containerPointToLatLng(L.point(0, 0));
const right = this._map.containerPointToLatLng(L.point(size.x, 0));
const sinPhi2 = Math.pow(Math.sin(center.lat * to_rad), 2);
const N = this._a / Math.sqrt(1.0 - this._e2 * sinPhi2);
const dl = this._interval / (N * Math.cos(center.lat * to_rad)) * to_deg;
// draw major ticks
for (let i = center.lng + dl / 2; i < right.lng; i = i + dl) this._draw_lon_tick(i, 10, weight * 1.5);
for (let i = center.lng - dl / 2; i > left.lng; i = i - dl) this._draw_lon_tick(i, 10, weight * 1.5);
// draw minor ticks
for (let i = center.lng; i < right.lng; i = i + dl / 10) this._draw_lon_tick(i, 4, weight);
for (let i = center.lng - dl / 10; i > left.lng; i = i - dl / 10) this._draw_lon_tick(i, 4, weight);
},
_setCanvasPosition: function() {
let lt = this._map.containerPointToLayerPoint([0, 0]);
/**
* @TODO add support for "leaflet-rotate"
*/
if (this._map._bearing) {
lt = this._map.rotatedPointToMapPanePoint(
this._map.containerPointToLayerPoint(L.point(this._map._container.getBoundingClientRect()))
);
}
L.DomUtil.setPosition(this._canvas, lt);
},
_latLngToCanvasPoint: function (latlng) {
return L.point(
this._map
.project(L.latLng(latlng))
._subtract(this._map.getPixelOrigin())
).add(this._map._getMapPanePos());
},
_draw_lat_tick: function (phi, lenght, weight) {
const to_deg = 180.0/Math.PI;
const size = this._map.getSize();
const y = this._latLngToCanvasPoint(L.latLng(phi * to_deg, 0.0)).y;
this._ctx.lineWidth = weight;
this._ctx.beginPath();
this._ctx.moveTo(0, y);
this._ctx.lineTo(+ lenght, y);
this._ctx.stroke();
},
_draw_lon_tick: function(lam, lenght, weight) {
const x = this._latLngToCanvasPoint(L.latLng(0.0, lam)).x;
this._ctx.lineWidth = weight;
this._ctx.beginPath();
this._ctx.moveTo(x, 0);
this._ctx.lineTo(x, lenght);
this._ctx.stroke();
},
_merLength: function(phi) {
const cos2 = Math.cos(2.0 * phi);
const sin2 = Math.sin(2.0 * phi);
return this._A * (phi + sin2 * (this._c1 + (this._c2 + (this._c3 + (this._c4 + this._c5 * cos2) * cos2) *cos2) * cos2));
},
_invmerLength: function(s) {
const psi = s/this._A;
const cos2 = Math.cos(2.0 * psi);
const sin2 = Math.sin(2.0 * psi);
return psi + sin2 * (this._ic1 + (this._ic2 + (this._ic3 + (this._ic4 + this._ic5 * cos2) * cos2) * cos2) * cos2);
},
});
L.control.edgeScale = function (options) {
return new L.Control.EdgeScale(options);
};
L.Map.mergeOptions({
edgeScaleControl: false
});
L.Map.addInitHook(function () {
if (this.options.edgeScaleControl) {
this.edgeScaleControl = new L.Control.EdgeScale();
this.addControl(this.edgeScaleControl);
}
});

File diff suppressed because one or more lines are too long

View File

@@ -1,347 +0,0 @@
/*
* https://github.com/adoroszlai/joebed/tree/gh-pages
*
* The MIT License (MIT)
*
* Copyright (c) 2014- Doroszlai Attila, 2019- Raruto
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
L.Mixin.Selectable = {
includes: L.Mixin.Events,
setSelected: function(s) {
var selected = !!s;
if (this._selected !== selected) {
this._selected = selected;
this.fire('selected');
}
},
isSelected: function() {
return !!this._selected;
},
};
L.Mixin.Selection = {
includes: L.Mixin.Events,
getSelection: function() {
return this._selected;
},
setSelection: function(item) {
if (this._selected === item) {
if (item !== null) {
item.setSelected(!item.isSelected());
if (!item.isSelected()) {
this._selected = null;
}
}
} else {
if (this._selected) {
this._selected.setSelected(false);
}
this._selected = item;
if (this._selected) {
this._selected.setSelected(true);
}
}
this.fire('selection_changed');
},
};
L.Control.LayersLegend = L.Control.Layers.extend({
_onInputClick: function() {
this._handlingClick = true;
this._layerControlInputs.reduceRight((_,input) => {
if (input.checked) {
this._map.fireEvent("legend_selected", {
layer: this._getLayer(input.layerId).layer,
input: input,
}, true);
return input;
}
}, 0);
this._handlingClick = false;
this._refocusOnMap();
}
});
L.control.layersLegend = (baseLayers, overlays, options) => new L.Control.LayersLegend(baseLayers, overlays, options);
L.GeoJSON.include(L.Mixin.Selectable);
L.GpxGroup = L.Class.extend({
options: {
highlight: {
color: '#ff0',
opacity: 1,
},
points: [],
points_options: {
icon: {
iconUrl: '../images/elevation-poi.png',
iconSize: [12, 12],
}
},
flyToBounds: true,
legend: false,
legend_options: {
position: "topright",
collapsed: false,
},
elevation: true,
elevation_options: {
theme: 'yellow-theme',
detached: true,
elevationDiv: '#elevation-div',
},
distanceMarkers: true,
distanceMarkers_options: {
lazy: true
},
},
initialize: function(tracks, options) {
L.Util.setOptions(this, options);
this._count = 0;
this._loadedCount = 0;
this._tracks = tracks;
this._layers = L.featureGroup();
this._markers = L.featureGroup();
this._elevation = L.control.elevation(this.options.elevation_options);
this._legend = L.control.layersLegend(null, null, this.options.legend_options);
this.options.points.forEach((poi) =>
L
.marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) })
.bindTooltip(poi.name, { direction: 'auto' }).addTo(this._markers)
);
},
getBounds: function() {
return this._layers.getBounds();
},
addTo: function(map) {
this._layers.addTo(map);
this._markers.addTo(map);
this._map = map;
this.on('selection_changed', this._onSelectionChanged, this);
this._map.on('legend_selected', this._onLegendSelected, this);
this._tracks.forEach(this.addTrack, this);
},
addTrack: function(track) {
if (track instanceof Object) {
this._loadGeoJSON(track);
} else {
fetch(track)
.then(response => response.ok && response.text())
.then(text => this._elevation._parseFromString(text))
.then(geojson => this._loadGeoJSON(geojson, track.split('/').pop().split('#')[0].split('?')[0]));
}
},
_loadGeoJSON: function(geojson, fallbackName) {
if (geojson) {
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
this._loadRoute(geojson);
}
},
_loadRoute: function(data) {
if (!data) return;
var line_style = {
color: this._uniqueColors(this._tracks.length)[this._count++],
opacity: 0.75,
weight: 5,
distanceMarkers: this.options.distanceMarkers_options,
};
var route = L.geoJson(data, {
name: data.name || '',
style: (feature) => line_style,
distanceMarkers: line_style.distanceMarkers,
originalStyle: line_style,
filter: feature => feature.geometry.type != "Point",
});
this._elevation.import(this._elevation.__LGEOMUTIL).then(() => {
route.addTo(this._layers);
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
this._onEachRouteLoaded(route);
});
},
_onEachRouteLayer: function(route, layer) {
var polyline = layer;
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
polyline.on('mouseout', L.bind(this._onRouteMouseOut, this, route, polyline));
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
polyline.bindTooltip(route.options.name, { direction: 'auto', sticky: true, });
},
_onEachRouteLoaded: function(route) {
if (this.options.legend) {
this._legend.addBaseLayer(route, '<svg id="legend_' + route._leaflet_id + '" width="25" height="10" version="1.1" xmlns="http://www.w3.org/2000/svg">' + '<line x1="0" x2="50" y1="5" y2="5" stroke="' + route.options.originalStyle.color + '" fill="transparent" stroke-width="5" /></svg>' + ' ' + route.options.name);
}
this.fire('route_loaded', { route: route });
if (++this._loadedCount === this._tracks.length) {
this.fire('loaded');
if (this.options.flyToBounds) {
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
}
if (this.options.legend) {
this._legend.addTo(this._map);
}
}
},
highlight: function(route, polyline) {
polyline.setStyle(this.options.highlight);
if (this.options.distanceMarkers) {
polyline.addDistanceMarkers();
}
},
unhighlight: function(route, polyline) {
polyline.setStyle(route.options.originalStyle);
if (this.options.distanceMarkers) {
polyline.removeDistanceMarkers();
}
},
_onRouteMouseOver: function(route, polyline) {
if (!route.isSelected()) {
this.highlight(route, polyline);
if (this.options.legend) {
this.setSelection(route);
L.DomUtil.get('legend_' + route._leaflet_id).parentNode.previousSibling.click();
}
}
this.fire('route_mouseover', { route: route, polyline: polyline });
},
_onRouteMouseOut: function(route, polyline) {
if (!route.isSelected()) {
this.unhighlight(route, polyline);
}
this.fire('route_mouseout', { route: route, polyline: polyline });
},
_onRouteClick: function(route, polyline) {
this.setSelection(route);
},
_onRouteSelected: function(route, polyline) {
if (!route.isSelected()) {
this.unhighlight(route, polyline);
}
},
_onSelectionChanged: function(e) {
var elevation = this._elevation;
var eleDiv = elevation.getContainer();
var route = this.getSelection();
elevation.clear();
if (route && route.isSelected()) {
if (!eleDiv) {
elevation.addTo(this._map);
}
route.getLayers().forEach(function(layer) {
if (layer instanceof L.Polyline) {
elevation.addData(layer, false);
layer.bringToFront();
}
});
} else {
if (eleDiv) {
elevation.remove();
}
}
},
_onLegendSelected: function(e) {
var parent = e.input.closest('.leaflet-control-layers-list');
var route = e.layer;
if (!route.isSelected()) {
this.setSelection(route);
for (var i in route._layers) {
this.highlight(route, route._layers[i]);
}
this._map.flyToBounds(e.layer.getBounds());
}
parent.scroll({ top: (e.input.offsetTop - parent.offsetTop) || 0, behavior: 'smooth' });
this._layers.eachLayer(layer => {
var legend = L.DomUtil.get('legend_' + layer._leaflet_id);
legend.querySelector("line").style.stroke = layer.isSelected() ? this.options.highlight.color : "";
legend.parentNode.style.fontWeight = layer.isSelected() ? "700" : "";
});
},
_uniqueColors: function(count) {
return count === 1 ? ['#0000ff'] : new Array(count).fill(null).map((_,i) => this._hsvToHex(i * (1 / count), 1, 0.7));
},
_hsvToHex: function(h, s, v) {
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
var rgb = { 0: [v, t, p], 1: [q, v, p], 2: [p, v, t], 3: [p, q, v], 4: [t, p, v], 5: [v, p, q] }[i % 6];
return rgb.map(d => d * 255).reduce((hex, byte) => hex + ((byte >> 4) & 0x0F).toString(16) + (byte & 0x0F).toString(16), "#");
},
removeFrom: function(map) {
this._layers.removeFrom(map);
},
});
L.GpxGroup.include(L.Mixin.Events);
L.GpxGroup.include(L.Mixin.Selection);
L.gpxGroup = (tracks, options) => new L.GpxGroup(tracks, options);

File diff suppressed because one or more lines are too long

View File

@@ -1,456 +0,0 @@
/*
(c) 2017, iosphere GmbH
Leaflet.hotline, a Leaflet plugin for drawing gradients along polylines.
https://github.com/iosphere/Leaflet.hotline/
*/
(function (root, plugin) {
/**
* UMD wrapper.
* When used directly in the Browser it expects Leaflet to be globally
* available as `L`. The plugin then adds itself to Leaflet.
* When used as a CommonJS module (e.g. with browserify) only the plugin
* factory gets exported, so one hast to call the factory manually and pass
* Leaflet as the only parameter.
* @see {@link https://github.com/umdjs/umd}
*/
if (typeof define === 'function' && define.amd) {
define(['leaflet'], plugin);
} else if (typeof exports === 'object') {
module.exports = plugin;
} else {
plugin(root.L);
}
}(window, function (L) {
// Plugin is already added to Leaflet
if (L.Hotline) {
return L;
}
/**
* Core renderer.
* @constructor
* @param {HTMLElement | string} canvas - &lt;canvas> element or its id
* to initialize the instance on.
*/
var Hotline = function (canvas) {
if (!(this instanceof Hotline)) { return new Hotline(canvas); }
var defaultPalette = {
0.0: 'green',
0.5: 'yellow',
1.0: 'red'
};
this._canvas = canvas = ('string' === typeof canvas)
? document.getElementById(canvas)
: canvas;
this._ctx = canvas.getContext('2d');
this._width = canvas.width;
this._height = canvas.height;
this._weight = 5;
this._outlineWidth = 1;
this._outlineColor = 'black';
this._min = 0;
this._max = 1;
this._data = [];
this.palette(defaultPalette);
};
Hotline.prototype = {
/**
* Sets the width of the canvas. Used when clearing the canvas.
* @param {number} width - Width of the canvas.
*/
width: function (width) {
this._width = width;
return this;
},
/**
* Sets the height of the canvas. Used when clearing the canvas.
* @param {number} height - Height of the canvas.
*/
height: function (height) {
this._height = height;
return this;
},
/**
* Sets the weight of the path.
* @param {number} weight - Weight of the path in px.
*/
weight: function (weight) {
this._weight = weight;
return this;
},
/**
* Sets the width of the outline around the path.
* @param {number} outlineWidth - Width of the outline in px.
*/
outlineWidth: function (outlineWidth) {
this._outlineWidth = outlineWidth;
return this;
},
/**
* Sets the color of the outline around the path.
* @param {string} outlineColor - A CSS color value.
*/
outlineColor: function (outlineColor) {
this._outlineColor = outlineColor;
return this;
},
/**
* Sets the palette gradient.
* @param {Object.<number, string>} palette - Gradient definition.
* e.g. { 0.0: 'white', 1.0: 'black' }
*/
palette: function (palette) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
gradient = ctx.createLinearGradient(0, 0, 0, 256);
canvas.width = 1;
canvas.height = 256;
for (var i in palette) {
gradient.addColorStop(i, palette[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, 256);
this._palette = ctx.getImageData(0, 0, 1, 256).data;
return this;
},
/**
* Sets the value used at the start of the palette gradient.
* @param {number} min
*/
min: function (min) {
this._min = min;
return this;
},
/**
* Sets the value used at the end of the palette gradient.
* @param {number} max
*/
max: function (max) {
this._max = max;
return this;
},
/**
* A path to rander as a hotline.
* @typedef Array.<{x:number, y:number, z:number}> Path - Array of x, y and z coordinates.
*/
/**
* Sets the data that gets drawn on the canvas.
* @param {(Path|Path[])} data - A single path or an array of paths.
*/
data: function (data) {
this._data = data;
return this;
},
/**
* Adds a path to the list of paths.
* @param {Path} path
*/
add: function (path) {
this._data.push(path);
return this;
},
/**
* Draws the currently set paths.
*/
draw: function () {
var ctx = this._ctx;
ctx.globalCompositeOperation = 'source-over';
ctx.lineCap = 'round';
this._drawOutline(ctx);
this._drawHotline(ctx);
return this;
},
/**
* Gets the RGB values of a given z value of the current palette.
* @param {number} value - Value to get the color for, should be between min and max.
* @returns {Array.<number>} The RGB values as an array [r, g, b]
*/
getRGBForValue: function (value) {
var valueRelative = Math.min(Math.max((value - this._min) / (this._max - this._min), 0), 0.999) || 0;
var paletteIndex = Math.floor(valueRelative * 256) * 4;
return [
this._palette[paletteIndex],
this._palette[paletteIndex + 1],
this._palette[paletteIndex + 2]
];
},
/**
* Draws the outline of the graphs.
* @private
*/
_drawOutline: function (ctx) {
var i, j, dataLength, path, pathLength, pointStart, pointEnd;
if (this._outlineWidth) {
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
path = this._data[i];
ctx.lineWidth = this._weight + 2 * this._outlineWidth;
for (j = 1, pathLength = path.length; j < pathLength; j++) {
pointStart = path[j - 1];
pointEnd = path[j];
ctx.strokeStyle = this._outlineColor;
ctx.beginPath();
ctx.moveTo(pointStart.x, pointStart.y);
ctx.lineTo(pointEnd.x, pointEnd.y);
ctx.stroke();
}
}
}
},
/**
* Draws the color encoded hotline of the graphs.
* @private
*/
_drawHotline: function (ctx) {
var i, j, dataLength, path, pathLength, pointStart, pointEnd,
gradient, gradientStartRGB, gradientEndRGB;
ctx.lineWidth = this._weight;
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
path = this._data[i];
for (j = 1, pathLength = path.length; j < pathLength; j++) {
pointStart = path[j - 1];
pointEnd = path[j];
// Create a gradient for each segment, pick start end end colors from palette gradient
gradient = ctx.createLinearGradient(pointStart.x, pointStart.y, pointEnd.x, pointEnd.y);
gradientStartRGB = this.getRGBForValue(pointStart.z);
gradientEndRGB = this.getRGBForValue(pointEnd.z);
gradient.addColorStop(0, 'rgb(' + gradientStartRGB.join(',') + ')');
gradient.addColorStop(1, 'rgb(' + gradientEndRGB.join(',') + ')');
ctx.strokeStyle = gradient;
ctx.beginPath();
ctx.moveTo(pointStart.x, pointStart.y);
ctx.lineTo(pointEnd.x, pointEnd.y);
ctx.stroke();
}
}
}
};
var Renderer = L.Canvas.extend({
_initContainer: function () {
L.Canvas.prototype._initContainer.call(this);
this._hotline = new Hotline(this._container);
},
_update: function () {
L.Canvas.prototype._update.call(this);
this._hotline.width(this._container.width);
this._hotline.height(this._container.height);
},
_updatePoly: function (layer) {
if (!this._drawing) { return; }
var parts = layer._parts;
if (!parts.length) { return; }
this._updateOptions(layer);
this._hotline
.data(parts)
.draw();
},
_updateOptions: function (layer) {
if (layer.options.min != null) {
this._hotline.min(layer.options.min);
}
if (layer.options.max != null) {
this._hotline.max(layer.options.max);
}
if (layer.options.weight != null) {
this._hotline.weight(layer.options.weight);
}
if (layer.options.outlineWidth != null) {
this._hotline.outlineWidth(layer.options.outlineWidth);
}
if (layer.options.outlineColor != null) {
this._hotline.outlineColor(layer.options.outlineColor);
}
if (layer.options.palette) {
this._hotline.palette(layer.options.palette);
}
}
});
var renderer = function (options) {
return L.Browser.canvas ? new Renderer(options) : null;
};
var Util = {
/**
* This is just a copy of the original Leaflet version that support a third z coordinate.
* @see {@link http://leafletjs.com/reference.html#lineutil-clipsegment|Leaflet}
*/
clipSegment: function (a, b, bounds, useLastCode, round) {
var codeA = useLastCode ? this._lastCode : L.LineUtil._getBitCode(a, bounds),
codeB = L.LineUtil._getBitCode(b, bounds),
codeOut, p, newCode;
// save 2nd code to avoid calculating it on the next segment
this._lastCode = codeB;
while (true) {
// if a,b is inside the clip window (trivial accept)
if (!(codeA | codeB)) {
return [a, b];
// if a,b is outside the clip window (trivial reject)
} else if (codeA & codeB) {
return false;
// other cases
} else {
codeOut = codeA || codeB;
p = L.LineUtil._getEdgeIntersection(a, b, codeOut, bounds, round);
newCode = L.LineUtil._getBitCode(p, bounds);
if (codeOut === codeA) {
p.z = a.z;
a = p;
codeA = newCode;
} else {
p.z = b.z;
b = p;
codeB = newCode;
}
}
}
}
};
L.Hotline = L.Polyline.extend({
statics: {
Renderer: Renderer,
renderer: renderer
},
options: {
renderer: renderer(),
min: 0,
max: 1,
palette: {
0.0: 'green',
0.5: 'yellow',
1.0: 'red'
},
weight: 5,
outlineColor: 'black',
outlineWidth: 1
},
getRGBForValue: function (value) {
return this._renderer._hotline.getRGBForValue(value);
},
/**
* Just like the Leaflet version, but with support for a z coordinate.
*/
_projectLatlngs: function (latlngs, result, projectedBounds) {
var flat = latlngs[0] instanceof L.LatLng,
len = latlngs.length,
i, ring;
if (flat) {
ring = [];
for (i = 0; i < len; i++) {
ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
// Add the altitude of the latLng as the z coordinate to the point
ring[i].z = latlngs[i].alt;
projectedBounds.extend(ring[i]);
}
result.push(ring);
} else {
for (i = 0; i < len; i++) {
this._projectLatlngs(latlngs[i], result, projectedBounds);
}
}
},
/**
* Just like the Leaflet version, but uses `Util.clipSegment()`.
*/
_clipPoints: function () {
if (this.options.noClip) {
this._parts = this._rings;
return;
}
this._parts = [];
var parts = this._parts,
bounds = this._renderer._bounds,
i, j, k, len, len2, segment, points;
for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
points = this._rings[i];
for (j = 0, len2 = points.length; j < len2 - 1; j++) {
segment = Util.clipSegment(points[j], points[j + 1], bounds, j, true);
if (!segment) { continue; }
parts[k] = parts[k] || [];
parts[k].push(segment[0]);
// if segment goes out of screen, or it's the last one, it's the end of the line part
if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
parts[k].push(segment[1]);
k++;
}
}
}
},
_clickTolerance: function () {
return this.options.weight / 2 + this.options.outlineWidth + (L.Browser.touch ? 10 : 0);
}
});
L.hotline = function (latlngs, options) {
return new L.Hotline(latlngs, options);
};
return L;
}));

File diff suppressed because one or more lines are too long

View File

@@ -1,183 +0,0 @@
/*
* Copyright (c) 2022, GPL-3.0+ Project, Raruto
*
* This file is free software: you may copy, redistribute and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) 2017 Goker Tanrisever
*
* Permission to use, copy, modify, and/or distribute this software
* for any purpose with or without fee is hereby granted, provided
* that the above copyright notice and this permission notice appear
* in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
L.Control.Ruler = L.Control.extend({
options: {
position: 'topright',
circleMarker: {
color: 'red',
radius: 2,
},
lineStyle: {
color: 'red',
dashArray: '1,6'
},
lengthUnit: {
display: 'km',
decimal: 2,
factor: 0.001, // meters -> kilometers
label: 'Distance:'
},
angleUnit: {
display: '&deg;',
decimal: 2,
factor: 360,
label: 'Bearing:'
}
},
initialize: function (options) {
L.setOptions(this, options);
this._layers = L.layerGroup();
this._enabled = false;
},
onAdd: function (map) {
this._defaultCursor = map._container.style.cursor;
this._map = map;
let container = L.DomUtil.create('div', 'leaflet-bar');
container.classList.add('leaflet-ruler');
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(container, 'click', this._toggleMeasure, this);
return this._container = container;
},
onRemove: function () {
L.DomEvent.off(this._container, 'click', this._toggleMeasure, this);
},
_attachMouseEvents: function () {
let map = this._map;
map.doubleClickZoom.disable();
L.DomEvent.on(map._container, 'keydown', this._escape, this);
L.DomEvent.on(map._container, 'dblclick', this._closePath, this);
map._container.style.cursor = 'crosshair';
map.on('click', this._addPoint, this);
map.on('mousemove', this._moving, this);
},
_removeMouseEvents: function () {
let map = this._map;
map.doubleClickZoom.enable();
L.DomEvent.off(map._container, 'keydown', this._escape, this);
L.DomEvent.off(map._container, 'dblclick', this._closePath, this);
map._container.style.cursor = this._defaultCursor;
map.off('click', this._addPoint, this);
map.off('mousemove', this._moving, this);
},
_disable: function () {
this._enabled = false;
this._container.classList.remove("leaflet-ruler-clicked");
this._layers.remove().clearLayers();
this._latlngs = [];
this._totalLength = 0;
this._removeMouseEvents();
},
_enable: function () {
this._enabled = true;
this._container.classList.add("leaflet-ruler-clicked");
this._circles = L.featureGroup().addTo(this._layers);
this._polyline = L.polyline([], this.options.lineStyle).addTo(this._layers);
this._layers.addTo(this._map);
this._latlngs = [];
this._totalLength = 0;
this._attachMouseEvents();
},
_toggleMeasure: function () {
this._enabled ? this._disable() : this._enable();
},
_drawTooltip: function (latlng, layer, incremental) {
let lastClick = this._latlngs[this._latlngs.length - 1] ?? latlng;
let bearing = this._calculateBearing(lastClick, latlng);
let distance = lastClick.distanceTo(latlng) * this.options.lengthUnit.factor;
let accumulated = this._totalLength + distance;
let totalLength = accumulated.toFixed(this.options.lengthUnit.decimal);
let plusLength = incremental ? '<br><div class="plus-length">(+' + distance.toFixed(this.options.lengthUnit.decimal) + ')</div>' : '';
this._totalLength = incremental ? this._totalLength : accumulated;
if (!layer.getTooltip()) layer.bindTooltip('', incremental ? { direction: "auto", sticky: true, offset: L.point(0, -40), className: 'moving-tooltip' } : { permanent: true, className: 'result-tooltip' }).openTooltip();
layer.setLatLng(latlng).setTooltipContent('<b>' + this.options.angleUnit.label + '</b>&nbsp;' + bearing.toFixed(this.options.angleUnit.decimal) + '&nbsp;' + this.options.angleUnit.display + '<br><b>' + this.options.lengthUnit.label + '</b>&nbsp;' + totalLength + '&nbsp;' + this.options.lengthUnit.display + plusLength);
},
_addPoint: function (e) {
let latlng = e.latlng || e;
let point = L.circleMarker(latlng, this.options.circleMarker).addTo(this._circles);
this._polyline.addLatLng(latlng);
if(this._latlngs.length && !latlng.equals(this._latlngs[this._latlngs.length - 1])){
this._drawTooltip(latlng, point, false);
}
this._latlngs.push(latlng);
},
_moving: function (e) {
if (this._latlngs.length) {
let lastCLick = this._latlngs[this._latlngs.length - 1];
if (!this._tempLine) this._tempLine = L.polyline([], this.options.lineStyle).addTo(this._map);
if (!this._tempPoint) this._tempPoint = L.circleMarker(e.latlng, this.options.circleMarker).addTo(this._map);
this._tempLine.setLatLngs([lastCLick, e.latlng]);
this._drawTooltip(e.latlng, this._tempPoint, true);
L.DomEvent.off(this._container, 'click', this._toggleMeasure, this);
}
},
_escape: function (e) {
if (e.keyCode === 27) {
if (this._latlngs.length) {
this._closePath();
} else {
this._enabled = true;
this._toggleMeasure();
}
}
},
_calculateBearing: function (start, end) {
const toRad = L.DomUtil.DEG_TO_RAD;
const toDeg = (this.options.angleUnit.factor / 2) / Math.PI;
let y = Math.sin((end.lng - start.lng) * toRad) * Math.cos(end.lat * toRad);
let x = Math.cos(start.lat * toRad) * Math.sin(end.lat * toRad) - Math.sin(start.lat * toRad) * Math.cos(end.lat * toRad) * Math.cos((end.lng - start.lng) * toRad);
return (Math.atan2(y, x) * toDeg + this.options.angleUnit.factor) % this.options.angleUnit.factor;
},
_closePath: function () {
if (this._tempLine) {
this._tempLine.remove();
this._tempLine = null;
}
if (this._tempPoint) {
this._tempPoint.remove();
this._tempLine = null;
}
if (this._latlngs.length <= 1) {
this._circles.remove();
}
this._enabled = false;
L.DomEvent.on(this._container, 'click', this._toggleMeasure, this);
this._toggleMeasure();
},
});
L.control.ruler = function (options) {
return new L.Control.Ruler(options);
};

View File

@@ -1 +0,0 @@
L.Control.Ruler=L.Control.extend({options:{position:"topright",circleMarker:{color:"red",radius:2},lineStyle:{color:"red",dashArray:"1,6"},lengthUnit:{display:"km",decimal:2,factor:.001,label:"Distance:"},angleUnit:{display:"&deg;",decimal:2,factor:360,label:"Bearing:"}},initialize:function(options){L.setOptions(this,options),this._layers=L.layerGroup(),this._enabled=!1},onAdd:function(map){this._defaultCursor=map._container.style.cursor,this._map=map;let container=L.DomUtil.create("div","leaflet-bar");return container.classList.add("leaflet-ruler"),L.DomEvent.disableClickPropagation(container),L.DomEvent.on(container,"click",this._toggleMeasure,this),this._container=container},onRemove:function(){L.DomEvent.off(this._container,"click",this._toggleMeasure,this)},_attachMouseEvents:function(){let map=this._map;map.doubleClickZoom.disable(),L.DomEvent.on(map._container,"keydown",this._escape,this),L.DomEvent.on(map._container,"dblclick",this._closePath,this),map._container.style.cursor="crosshair",map.on("click",this._addPoint,this),map.on("mousemove",this._moving,this)},_removeMouseEvents:function(){let map=this._map;map.doubleClickZoom.enable(),L.DomEvent.off(map._container,"keydown",this._escape,this),L.DomEvent.off(map._container,"dblclick",this._closePath,this),map._container.style.cursor=this._defaultCursor,map.off("click",this._addPoint,this),map.off("mousemove",this._moving,this)},_disable:function(){this._enabled=!1,this._container.classList.remove("leaflet-ruler-clicked"),this._map.removeLayer(this._layers),this._layers=L.layerGroup(),this._latlngs=[],this._totalLength=0,this._removeMouseEvents()},_enable:function(){this._enabled=!0,this._container.classList.add("leaflet-ruler-clicked"),this._circles=L.featureGroup().addTo(this._layers),this._polyline=L.polyline([],this.options.lineStyle).addTo(this._layers),this._layers.addTo(this._map),this._latlngs=[],this._totalLength=0,this._attachMouseEvents()},_toggleMeasure:function(){this._enabled?this._disable():this._enable()},_drawTooltip:function(latlng,layer,incremental){let clickCount=this._latlngs.length,lastClick=clickCount?this._latlngs[clickCount-1]:latlng,bearing=this._calculateBearing(lastClick,latlng),distance=lastClick.distanceTo(latlng)*this.options.lengthUnit.factor,totalLength,plusLength;incremental?(totalLength=(clickCount?distance+this._totalLength||0:distance).toFixed(this.options.lengthUnit.decimal),plusLength=clickCount?'<br><div class="plus-length">(+'+distance.toFixed(this.options.lengthUnit.decimal)+")</div>":""):(this._totalLength+=distance,totalLength=(clickCount?this._totalLength:distance).toFixed(this.options.lengthUnit.decimal),plusLength="");var text="<b>"+this.options.angleUnit.label+"</b>&nbsp;"+bearing.toFixed(this.options.angleUnit.decimal)+"&nbsp;"+this.options.angleUnit.display+"<br><b>"+this.options.lengthUnit.label+"</b>&nbsp;"+totalLength+"&nbsp;"+this.options.lengthUnit.display+plusLength;layer.setLatLng(latlng);let tooltip=layer.getTooltip();return tooltip?tooltip.setTooltipContent(text):layer.bindTooltip(text,incremental?{direction:"auto",sticky:!0,offset:L.point(0,-40),className:"moving-tooltip"}:{permanent:!0,className:"result-tooltip"}).openTooltip(),layer},_addPoint:function(e){let latlng=e.latlng||e,point=L.circleMarker(latlng,this.options.circleMarker).addTo(this._circles);this._polyline.addLatLng(latlng),this._latlngs.length&&!latlng.equals(this._latlngs[this._latlngs.length-1])&&this._drawTooltip(latlng,point,!1),this._latlngs.push(latlng)},_moving:function(e){if(this._latlngs.length){let lastCLick=this._latlngs[this._latlngs.length-1];this._tempLine||(this._tempLine=L.polyline([],this.options.lineStyle).addTo(this._map)),this._tempPoint||(this._tempPoint=L.circleMarker(e.latlng,this.options.circleMarker).addTo(this._map)),this._tempLine.setLatLngs([lastCLick,e.latlng]),this._drawTooltip(e.latlng,this._tempPoint,!0),L.DomEvent.off(this._container,"click",this._toggleMeasure,this)}},_escape:function(e){27===e.keyCode&&(this._latlngs.length?this._closePath():(this._enabled=!0,this._toggleMeasure()))},_calculateBearing:function(start,end){const toRad=L.DomUtil.DEG_TO_RAD,toDeg=this.options.angleUnit.factor/2/Math.PI;let y=Math.sin((end.lng-start.lng)*toRad)*Math.cos(end.lat*toRad),x=Math.cos(start.lat*toRad)*Math.sin(end.lat*toRad)-Math.sin(start.lat*toRad)*Math.cos(end.lat*toRad)*Math.cos((end.lng-start.lng)*toRad);return(Math.atan2(y,x)*toDeg+this.options.angleUnit.factor)%this.options.angleUnit.factor},_closePath:function(){this._tempLine&&(this._map.removeLayer(this._tempLine),this._tempLine=null),this._tempPoint&&(this._map.removeLayer(this._tempPoint),this._tempLine=null),this._latlngs.length<=1&&this._map.removeLayer(this._circles),this._enabled=!1,L.DomEvent.on(this._container,"click",this._toggleMeasure,this),this._toggleMeasure()}}),L.control.ruler=function(options){return new L.Control.Ruler(options)};

View File

@@ -1,150 +0,0 @@
L.Map.mergeOptions({
// @option almostOver: Boolean = true
// Set it to false to disable this plugin
almostOver: true,
// @option almostDistance: Number = 25
// Tolerance in pixels
almostDistance: 25, // pixels
// @option almostSamplingPeriod: Number = 50
// To reduce the 'mousemove' event frequency. In milliseconds
almostSamplingPeriod: 50, // ms
// @option almostOnMouseMove Boolean = true
// Set it to false to disable track 'mousemove' events and improve performance
// if AlmostOver is only need for 'click' events.
almostOnMouseMove: true,
});
L.Handler.AlmostOver = L.Handler.extend({
includes: L.Evented || L.Mixin.Events,
initialize: function (map) {
this._map = map;
this._layers = [];
this._previous = null;
this._marker = null;
this._buffer = 0;
// Reduce 'mousemove' event frequency
this.__mouseMoveSampling = (function () {
var timer = new Date();
return function (e) {
var date = new Date(),
filtered = (date - timer) < this._map.options.almostSamplingPeriod;
if (filtered || this._layers.length === 0) {
return; // Ignore movement
}
timer = date;
this._map.fire('mousemovesample', {latlng: e.latlng});
};
})();
},
addHooks: function () {
if (this._map.options.almostOnMouseMove) {
this._map.on('mousemove', this.__mouseMoveSampling, this);
this._map.on('mousemovesample', this._onMouseMove, this);
}
this._map.on('click dblclick', this._onMouseClick, this);
var map = this._map;
function computeBuffer() {
this._buffer = this._map.layerPointToLatLng([0, 0]).lat -
this._map.layerPointToLatLng([this._map.options.almostDistance,
this._map.options.almostDistance]).lat;
}
this._map.on('viewreset zoomend', computeBuffer, this);
this._map.whenReady(computeBuffer, this);
},
removeHooks: function () {
this._map.off('mousemovesample');
this._map.off('mousemove', this.__mouseMoveSampling, this);
this._map.off('click dblclick', this._onMouseClick, this);
},
addLayer: function (layer) {
if (typeof layer.eachLayer == 'function') {
layer.eachLayer(function (l) {
this.addLayer(l);
}, this);
}
else {
if (typeof this.indexLayer == 'function') {
this.indexLayer(layer);
}
this._layers.push(layer);
}
},
removeLayer: function (layer) {
if (typeof layer.eachLayer == 'function') {
layer.eachLayer(function (l) {
this.removeLayer(l);
}, this);
}
else {
if (typeof this.unindexLayer == 'function') {
this.unindexLayer(layer);
}
var index = this._layers.indexOf(layer);
if (0 <= index) {
this._layers.splice(index, 1);
}
}
this._previous = null;
},
getClosest: function (latlng) {
var snapfunc = L.GeometryUtil.closestLayerSnap,
distance = this._map.options.almostDistance;
var snaplist = [];
if (typeof this.searchBuffer == 'function') {
snaplist = this.searchBuffer(latlng, this._buffer);
}
else {
snaplist = this._layers;
}
return snapfunc(this._map, snaplist, latlng, distance, false);
},
_onMouseMove: function (e) {
var closest = this.getClosest(e.latlng);
if (closest) {
if (!this._previous) {
this._map.fire('almost:over', {layer: closest.layer,
latlng: closest.latlng});
}
else if (L.stamp(this._previous.layer) != L.stamp(closest.layer)) {
this._map.fire('almost:out', {layer: this._previous.layer});
this._map.fire('almost:over', {layer: closest.layer,
latlng: closest.latlng});
}
this._map.fire('almost:move', {layer: closest.layer,
latlng: closest.latlng});
}
else {
if (this._previous) {
this._map.fire('almost:out', {layer: this._previous.layer});
}
}
this._previous = closest;
},
_onMouseClick: function (e) {
var closest = this.getClosest(e.latlng);
if (closest) {
this._map.fire('almost:' + e.type, {layer: closest.layer,
latlng: closest.latlng});
}
},
});
if (L.LayerIndexMixin !== undefined) {
L.Handler.AlmostOver.include(L.LayerIndexMixin);
}
L.Map.addInitHook('addHandler', 'almostOver', L.Handler.AlmostOver);

View File

@@ -1,767 +0,0 @@
// Packaging/modules magic dance.
(function (factory) {
var L;
if (typeof define === 'function' && define.amd) {
// AMD
define(['leaflet'], factory);
} else if (typeof module !== 'undefined') {
// Node/CommonJS
L = require('leaflet');
module.exports = factory(L);
} else {
// Browser globals
if (typeof window.L === 'undefined')
throw 'Leaflet must be loaded first';
factory(window.L);
}
}(function (L) {
"use strict";
L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) {
// true if it's a flat array of latlngs; false if nested
return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
};
/**
* @fileOverview Leaflet Geometry utilities for distances and linear referencing.
* @name L.GeometryUtil
*/
L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Shortcut function for planar distance between two {L.LatLng} at current zoom.
@tutorial distance-length
@param {L.Map} map Leaflet map to be used for this method
@param {L.LatLng} latlngA geographical point A
@param {L.LatLng} latlngB geographical point B
@returns {Number} planar distance
*/
distance: function (map, latlngA, latlngB) {
return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
},
/**
Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
@param {L.Map} map Leaflet map to be used for this method
@param {L.LatLng} latlng - The position to search
@param {L.LatLng} latlngA geographical point A of the segment
@param {L.LatLng} latlngB geographical point B of the segment
@returns {Number} planar distance
*/
distanceSegment: function (map, latlng, latlngA, latlngB) {
var p = map.latLngToLayerPoint(latlng),
p1 = map.latLngToLayerPoint(latlngA),
p2 = map.latLngToLayerPoint(latlngB);
return L.LineUtil.pointToSegmentDistance(p, p1, p2);
},
/**
Shortcut function for converting distance to readable distance.
@param {Number} distance distance to be converted
@param {String} unit 'metric' or 'imperial'
@returns {String} in yard or miles
*/
readableDistance: function (distance, unit) {
var isMetric = (unit !== 'imperial'),
distanceStr;
if (isMetric) {
// show metres when distance is < 1km, then show km
if (distance > 1000) {
distanceStr = (distance / 1000).toFixed(2) + ' km';
}
else {
distanceStr = distance.toFixed(1) + ' m';
}
}
else {
distance *= 1.09361;
if (distance > 1760) {
distanceStr = (distance / 1760).toFixed(2) + ' miles';
}
else {
distanceStr = distance.toFixed(1) + ' yd';
}
}
return distanceStr;
},
/**
Returns true if the latlng belongs to segment A-B
@param {L.LatLng} latlng - The position to search
@param {L.LatLng} latlngA geographical point A of the segment
@param {L.LatLng} latlngB geographical point B of the segment
@param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
@returns {boolean}
*/
belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
tolerance = tolerance === undefined ? 0.2 : tolerance;
var hypotenuse = latlngA.distanceTo(latlngB),
delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
return delta/hypotenuse < tolerance;
},
/**
* Returns total length of line
* @tutorial distance-length
*
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
* @returns {Number} Total length (pixels for Point, meters for LatLng)
*/
length: function (coords) {
var accumulated = L.GeometryUtil.accumulatedLengths(coords);
return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
},
/**
* Returns a list of accumulated length along a line.
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
* @returns {Array<Number>} Array of accumulated lengths (pixels for Point, meters for LatLng)
*/
accumulatedLengths: function (coords) {
if (typeof coords.getLatLngs == 'function') {
coords = coords.getLatLngs();
}
if (coords.length === 0)
return [];
var total = 0,
lengths = [0];
for (var i = 0, n = coords.length - 1; i< n; i++) {
total += coords[i].distanceTo(coords[i+1]);
lengths.push(total);
}
return lengths;
},
/**
Returns the closest point of a {L.LatLng} on the segment (A-B)
@tutorial closest
@param {L.Map} map Leaflet map to be used for this method
@param {L.LatLng} latlng - The position to search
@param {L.LatLng} latlngA geographical point A of the segment
@param {L.LatLng} latlngB geographical point B of the segment
@returns {L.LatLng} Closest geographical point
*/
closestOnSegment: function (map, latlng, latlngA, latlngB) {
var maxzoom = map.getMaxZoom();
if (maxzoom === Infinity)
maxzoom = map.getZoom();
var p = map.project(latlng, maxzoom),
p1 = map.project(latlngA, maxzoom),
p2 = map.project(latlngB, maxzoom),
closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
return map.unproject(closest, maxzoom);
},
/**
Returns the closest latlng on layer.
Accept nested arrays
@tutorial closest
@param {L.Map} map Leaflet map to be used for this method
@param {Array<L.LatLng>|Array<Array<L.LatLng>>|L.PolyLine|L.Polygon} layer - Layer that contains the result
@param {L.LatLng} latlng - The position to search
@param {?boolean} [vertices=false] - Whether to restrict to path vertices.
@returns {L.LatLng} Closest geographical point or null if layer param is incorrect
*/
closest: function (map, layer, latlng, vertices) {
var latlngs,
mindist = Infinity,
result = null,
i, n, distance, subResult;
if (layer instanceof Array) {
// if layer is Array<Array<T>>
if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
// if we have nested arrays, we calc the closest for each array
// recursive
for (i = 0; i < layer.length; i++) {
subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
if (subResult && subResult.distance < mindist) {
mindist = subResult.distance;
result = subResult;
}
}
return result;
} else if (layer[0] instanceof L.LatLng
|| typeof layer[0][0] === 'number'
|| typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
layer = L.polyline(layer);
} else {
return result;
}
}
// if we don't have here a Polyline, that means layer is incorrect
// see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23
if (! ( layer instanceof L.Polyline ) )
return result;
// deep copy of latlngs
latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
// add the last segment for L.Polygon
if (layer instanceof L.Polygon) {
// add the last segment for each child that is a nested array
var addLastSegment = function(latlngs) {
if (L.Polyline._flat(latlngs)) {
latlngs.push(latlngs[0]);
} else {
for (var i = 0; i < latlngs.length; i++) {
addLastSegment(latlngs[i]);
}
}
};
addLastSegment(latlngs);
}
// we have a multi polygon / multi polyline / polygon with holes
// use recursive to explore and return the good result
if ( ! L.Polyline._flat(latlngs) ) {
for (i = 0; i < latlngs.length; i++) {
// if we are at the lower level, and if we have a L.Polygon, we add the last segment
subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
if (subResult.distance < mindist) {
mindist = subResult.distance;
result = subResult;
}
}
return result;
} else {
// Lookup vertices
if (vertices) {
for(i = 0, n = latlngs.length; i < n; i++) {
var ll = latlngs[i];
distance = L.GeometryUtil.distance(map, latlng, ll);
if (distance < mindist) {
mindist = distance;
result = ll;
result.distance = distance;
}
}
return result;
}
// Keep the closest point of all segments
for (i = 0, n = latlngs.length; i < n-1; i++) {
var latlngA = latlngs[i],
latlngB = latlngs[i+1];
distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
if (distance <= mindist) {
mindist = distance;
result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
result.distance = distance;
}
}
return result;
}
},
/**
Returns the closest layer to latlng among a list of layers.
@tutorial closest
@param {L.Map} map Leaflet map to be used for this method
@param {Array<L.ILayer>} layers Set of layers
@param {L.LatLng} latlng - The position to search
@returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
*/
closestLayer: function (map, layers, latlng) {
var mindist = Infinity,
result = null,
ll = null,
distance = Infinity;
for (var i = 0, n = layers.length; i < n; i++) {
var layer = layers[i];
if (layer instanceof L.LayerGroup) {
// recursive
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
if (subResult.distance < mindist) {
mindist = subResult.distance;
result = subResult;
}
} else {
// Single dimension, snap on points, else snap on closest
if (typeof layer.getLatLng == 'function') {
ll = layer.getLatLng();
distance = L.GeometryUtil.distance(map, latlng, ll);
}
else {
ll = L.GeometryUtil.closest(map, layer, latlng);
if (ll) distance = ll.distance; // Can return null if layer has no points.
}
if (distance < mindist) {
mindist = distance;
result = {layer: layer, latlng: ll, distance: distance};
}
}
}
return result;
},
/**
Returns the n closest layers to latlng among a list of input layers.
@param {L.Map} map - Leaflet map to be used for this method
@param {Array<L.ILayer>} layers - Set of layers
@param {L.LatLng} latlng - The position to search
@param {?Number} [n=layers.length] - the expected number of output layers.
@returns {Array<object>} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n)
*/
nClosestLayers: function (map, layers, latlng, n) {
n = typeof n === 'number' ? n : layers.length;
if (n < 1 || layers.length < 1) {
return null;
}
var results = [];
var distance, ll;
for (var i = 0, m = layers.length; i < m; i++) {
var layer = layers[i];
if (layer instanceof L.LayerGroup) {
// recursive
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
results.push(subResult);
} else {
// Single dimension, snap on points, else snap on closest
if (typeof layer.getLatLng == 'function') {
ll = layer.getLatLng();
distance = L.GeometryUtil.distance(map, latlng, ll);
}
else {
ll = L.GeometryUtil.closest(map, layer, latlng);
if (ll) distance = ll.distance; // Can return null if layer has no points.
}
results.push({layer: layer, latlng: ll, distance: distance});
}
}
results.sort(function(a, b) {
return a.distance - b.distance;
});
if (results.length > n) {
return results.slice(0, n);
} else {
return results;
}
},
/**
* Returns all layers within a radius of the given position, in an ascending order of distance.
@param {L.Map} map Leaflet map to be used for this method
@param {Array<ILayer>} layers - A list of layers.
@param {L.LatLng} latlng - The position to search
@param {?Number} [radius=Infinity] - Search radius in pixels
@return {object[]} an array of objects including layer within the radius, closest latlng, and distance
*/
layersWithin: function(map, layers, latlng, radius) {
radius = typeof radius == 'number' ? radius : Infinity;
var results = [];
var ll = null;
var distance = 0;
for (var i = 0, n = layers.length; i < n; i++) {
var layer = layers[i];
if (typeof layer.getLatLng == 'function') {
ll = layer.getLatLng();
distance = L.GeometryUtil.distance(map, latlng, ll);
}
else {
ll = L.GeometryUtil.closest(map, layer, latlng);
if (ll) distance = ll.distance; // Can return null if layer has no points.
}
if (ll && distance < radius) {
results.push({layer: layer, latlng: ll, distance: distance});
}
}
var sortedResults = results.sort(function(a, b) {
return a.distance - b.distance;
});
return sortedResults;
},
/**
Returns the closest position from specified {LatLng} among specified layers,
with a maximum tolerance in pixels, providing snapping behaviour.
@tutorial closest
@param {L.Map} map Leaflet map to be used for this method
@param {Array<ILayer>} layers - A list of layers to snap on.
@param {L.LatLng} latlng - The position to snap
@param {?Number} [tolerance=Infinity] - Maximum number of pixels.
@param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex)
@returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
*/
closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
withVertices = typeof withVertices == 'boolean' ? withVertices : true;
var result = L.GeometryUtil.closestLayer(map, layers, latlng);
if (!result || result.distance > tolerance)
return null;
// If snapped layer is linear, try to snap on vertices (extremities and middle points)
if (withVertices && typeof result.layer.getLatLngs == 'function') {
var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
if (closest.distance < tolerance) {
result.latlng = closest;
result.distance = L.GeometryUtil.distance(map, closest, latlng);
}
}
return result;
},
/**
Returns the Point located on a segment at the specified ratio of the segment length.
@param {L.Point} pA coordinates of point A
@param {L.Point} pB coordinates of point B
@param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
@returns {L.Point} the interpolated point.
*/
interpolateOnPointSegment: function (pA, pB, ratio) {
return L.point(
(pA.x * (1 - ratio)) + (ratio * pB.x),
(pA.y * (1 - ratio)) + (ratio * pB.y)
);
},
/**
Returns the coordinate of the point located on a line at the specified ratio of the line length.
@param {L.Map} map Leaflet map to be used for this method
@param {Array<L.LatLng>|L.PolyLine} latlngs Set of geographical points
@param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive
@returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
(-1 if the interpolated point is the first vertex)
*/
interpolateOnLine: function (map, latLngs, ratio) {
latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
var n = latLngs.length;
if (n < 2) {
return null;
}
// ensure the ratio is between 0 and 1;
ratio = Math.max(Math.min(ratio, 1), 0);
if (ratio === 0) {
return {
latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]),
predecessor: -1
};
}
if (ratio == 1) {
return {
latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]),
predecessor: latLngs.length - 2
};
}
// project the LatLngs as Points,
// and compute total planar length of the line at max precision
var maxzoom = map.getMaxZoom();
if (maxzoom === Infinity)
maxzoom = map.getZoom();
var pts = [];
var lineLength = 0;
for(var i = 0; i < n; i++) {
pts[i] = map.project(latLngs[i], maxzoom);
if(i > 0)
lineLength += pts[i-1].distanceTo(pts[i]);
}
var ratioDist = lineLength * ratio;
// follow the line segments [ab], adding lengths,
// until we find the segment where the points should lie on
var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0;
for (var i = 0; cumulativeDistanceToB < ratioDist; i++) {
var pointA = pts[i], pointB = pts[i+1];
cumulativeDistanceToA = cumulativeDistanceToB;
cumulativeDistanceToB += pointA.distanceTo(pointB);
}
if (pointA == undefined && pointB == undefined) { // Happens when line has no length
var pointA = pts[0], pointB = pts[1], i = 1;
}
// compute the ratio relative to the segment [ab]
var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0;
var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio);
return {
latLng: map.unproject(interpolatedPoint, maxzoom),
predecessor: i-1
};
},
/**
Returns a float between 0 and 1 representing the location of the
closest point on polyline to the given latlng, as a fraction of total line length.
(opposite of L.GeometryUtil.interpolateOnLine())
@param {L.Map} map Leaflet map to be used for this method
@param {L.PolyLine} polyline Polyline on which the latlng will be search
@param {L.LatLng} latlng The position to search
@returns {Number} Float between 0 and 1
*/
locateOnLine: function (map, polyline, latlng) {
var latlngs = polyline.getLatLngs();
if (latlng.equals(latlngs[0]))
return 0.0;
if (latlng.equals(latlngs[latlngs.length-1]))
return 1.0;
var point = L.GeometryUtil.closest(map, polyline, latlng, false),
lengths = L.GeometryUtil.accumulatedLengths(latlngs),
total_length = lengths[lengths.length-1],
portion = 0,
found = false;
for (var i=0, n = latlngs.length-1; i < n; i++) {
var l1 = latlngs[i],
l2 = latlngs[i+1];
portion = lengths[i];
if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) {
portion += l1.distanceTo(point);
found = true;
break;
}
}
if (!found) {
throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
}
return portion / total_length;
},
/**
Returns a clone with reversed coordinates.
@param {L.PolyLine} polyline polyline to reverse
@returns {L.PolyLine} polyline reversed
*/
reverse: function (polyline) {
return L.polyline(polyline.getLatLngs().slice(0).reverse());
},
/**
Returns a sub-part of the polyline, from start to end.
If start is superior to end, returns extraction from inverted line.
@param {L.Map} map Leaflet map to be used for this method
@param {L.PolyLine} polyline Polyline on which will be extracted the sub-part
@param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
@param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
@returns {Array<L.LatLng>} new polyline
*/
extract: function (map, polyline, start, end) {
if (start > end) {
return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
}
// Bound start and end to [0-1]
start = Math.max(Math.min(start, 1), 0);
end = Math.max(Math.min(end, 1), 0);
var latlngs = polyline.getLatLngs(),
startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
// Return single point if start == end
if (start == end) {
var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
return [point.latLng];
}
// Array.slice() works indexes at 0
if (startpoint.predecessor == -1)
startpoint.predecessor = 0;
if (endpoint.predecessor == -1)
endpoint.predecessor = 0;
var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
result.unshift(startpoint.latLng);
result.push(endpoint.latLng);
return result;
},
/**
Returns true if first polyline ends where other second starts.
@param {L.PolyLine} polyline First polyline
@param {L.PolyLine} other Second polyline
@returns {bool}
*/
isBefore: function (polyline, other) {
if (!other) return false;
var lla = polyline.getLatLngs(),
llb = other.getLatLngs();
return (lla[lla.length-1]).equals(llb[0]);
},
/**
Returns true if first polyline starts where second ends.
@param {L.PolyLine} polyline First polyline
@param {L.PolyLine} other Second polyline
@returns {bool}
*/
isAfter: function (polyline, other) {
if (!other) return false;
var lla = polyline.getLatLngs(),
llb = other.getLatLngs();
return (lla[0]).equals(llb[llb.length-1]);
},
/**
Returns true if first polyline starts where second ends or start.
@param {L.PolyLine} polyline First polyline
@param {L.PolyLine} other Second polyline
@returns {bool}
*/
startsAtExtremity: function (polyline, other) {
if (!other) return false;
var lla = polyline.getLatLngs(),
llb = other.getLatLngs(),
start = lla[0];
return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
},
/**
Returns horizontal angle in degres between two points.
@param {L.Point} a Coordinates of point A
@param {L.Point} b Coordinates of point B
@returns {Number} horizontal angle
*/
computeAngle: function(a, b) {
return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
},
/**
Returns slope (Ax+B) between two points.
@param {L.Point} a Coordinates of point A
@param {L.Point} b Coordinates of point B
@returns {Object} with ``a`` and ``b`` properties.
*/
computeSlope: function(a, b) {
var s = (b.y - a.y) / (b.x - a.x),
o = a.y - (s * a.x);
return {'a': s, 'b': o};
},
/**
Returns LatLng of rotated point around specified LatLng center.
@param {L.LatLng} latlngPoint: point to rotate
@param {double} angleDeg: angle to rotate in degrees
@param {L.LatLng} latlngCenter: center of rotation
@returns {L.LatLng} rotated point
*/
rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
var maxzoom = map.getMaxZoom();
if (maxzoom === Infinity)
maxzoom = map.getZoom();
var angleRad = angleDeg*Math.PI/180,
pPoint = map.project(latlngPoint, maxzoom),
pCenter = map.project(latlngCenter, maxzoom),
x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
return map.unproject(new L.Point(x2,y2), maxzoom);
},
/**
Returns the bearing in degrees clockwise from north (0 degrees)
from the first L.LatLng to the second, at the first LatLng
@param {L.LatLng} latlng1: origin point of the bearing
@param {L.LatLng} latlng2: destination point of the bearing
@returns {float} degrees clockwise from north.
*/
bearing: function(latlng1, latlng2) {
var rad = Math.PI / 180,
lat1 = latlng1.lat * rad,
lat2 = latlng2.lat * rad,
lon1 = latlng1.lng * rad,
lon2 = latlng2.lng * rad,
y = Math.sin(lon2 - lon1) * Math.cos(lat2),
x = Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360;
return bearing >= 180 ? bearing-360 : bearing;
},
/**
Returns the point that is a distance and heading away from
the given origin point.
@param {L.LatLng} latlng: origin point
@param {float} heading: heading in degrees, clockwise from 0 degrees north.
@param {float} distance: distance in meters
@returns {L.latLng} the destination point.
Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
for a great reference and examples.
*/
destination: function(latlng, heading, distance) {
heading = (heading + 360) % 360;
var rad = Math.PI / 180,
radInv = 180 / Math.PI,
R = 6378137, // approximation of Earth's radius
lon1 = latlng.lng * rad,
lat1 = latlng.lat * rad,
rheading = heading * rad,
sinLat1 = Math.sin(lat1),
cosLat1 = Math.cos(lat1),
cosDistR = Math.cos(distance / R),
sinDistR = Math.sin(distance / R),
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
sinDistR * Math.cos(rheading)),
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
lon2 = lon2 * radInv;
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
return L.latLng([lat2 * radInv, lon2]);
},
/**
Returns the the angle of the given segment and the Equator in degrees,
clockwise from 0 degrees north.
@param {L.Map} map: Leaflet map to be used for this method
@param {L.LatLng} latlngA: geographical point A of the segment
@param {L.LatLng} latlngB: geographical point B of the segment
@returns {Float} the angle in degrees.
*/
angle: function(map, latlngA, latlngB) {
var pointA = map.latLngToContainerPoint(latlngA),
pointB = map.latLngToContainerPoint(latlngB),
angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90;
angleDeg += angleDeg < 0 ? 360 : 0;
return angleDeg;
},
/**
Returns a point snaps on the segment and heading away from the given origin point a distance.
@param {L.Map} map: Leaflet map to be used for this method
@param {L.LatLng} latlngA: geographical point A of the segment
@param {L.LatLng} latlngB: geographical point B of the segment
@param {float} distance: distance in meters
@returns {L.latLng} the destination point.
*/
destinationOnSegment: function(map, latlngA, latlngB, distance) {
var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB),
latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance);
return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
},
});
return L.GeometryUtil;
}));

File diff suppressed because one or more lines are too long

View File

@@ -1,783 +0,0 @@
import * as D3 from './d3.js';
const _ = L.Control.Elevation.Utils;
export var Chart = L.Control.Elevation.Chart = L.Class.extend({
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
initialize(opts, control) {
this.options = opts;
this.control = control;
this._data = control._data || [];
// cache registered components
this._props = {
scales : {},
paths : {},
areas : {},
grids : {},
axes : {},
legendItems : {},
tooltipItems: {},
};
this._scales = {};
this._domains = {};
this._ranges = {};
this._paths = {};
this._brushEnabled = opts.dragging;
this._zoomEnabled = opts.zooming;
opts.xTicks = this._xTicks();
opts.yTicks = this._yTicks();
let chart = this._chart = D3.Chart(opts);
// SVG Container
this._container = chart.svg;
// Panes
this._grid = chart.pane('grid');
this._area = chart.pane('area');
this._point = chart.pane('point');
this._axis = chart.pane('axis');
this._legend = chart.pane('legend');
this._tooltip = chart.pane('tooltip');
this._ruler = chart.pane('ruler');
// Scales
this._initScale();
// Helpers
this._mask = chart.get('mask');
this._context = chart.get('context');
this._brush = chart.get('brush');
this._zoom = d3.zoom();
this._drag = d3.drag();
// Interactions
this._initInteractions();
// svg.on('resize', (e)=>console.log(e.detail));
// Handle multi-track segments
this._maskGaps = [];
control.on('eletrack_added', ({index}) => {
this._maskGaps.push(index);
control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
});
},
update(props) {
if (props) {
if (props.data) this._data = props.data;
if (props.options) this.options = props.options;
}
this.options.xTicks = this._xTicks();
this.options.yTicks = this._yTicks();
this._updateScale();
this._updateAxis();
this._updateMargins();
this._updateLegend();
this._updateClipper();
this._updateArea();
return this;
},
render() {
return container => container.append(() => this._container.node());
},
clear() {
this._resetDrag();
this._hideDiagramIndicator()
this._area.selectAll('path').attr("d", "M0 0");
this._context.clearRect(0, 0, this._width(), this._height());
this._mask.selectAll(".gap").remove();
this._maskGaps = [];
// if (this._path) {
// this._x.domain([0, 1]);
// this._y.domain([0, 1]);
// }
},
_drawPath(name) {
let path = this._paths[name];
let area = this._props.areas[name];
path.datum(this._data).attr("d",
D3.Area(
L.extend({}, area, {
width : this._width(),
height : this._height(),
scaleX : this._scales[area.scaleX],
scaleY : this._scales[area.scaleY]
})
)
);
if (!path.classed('leaflet-hidden')) {
this.options.preferCanvas
? _.drawCanvas(this._context, path)
: _.append(this._area.node(), path.node());
}
},
_hasActiveLayers() {
const paths = this._paths;
for (var i in paths) {
if (!paths[i].classed('leaflet-hidden')) {
return true;
}
}
return false;
},
/**
* Initialize "d3-brush".
*/
_initBrush(e) {
const brush = ({selection}) => {
if (selection && this._data.length) {
let start = this._findIndexForXCoord(selection[0]);
let end = this._findIndexForXCoord(selection[1]);
this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
}
}
const focus = (e) => {
if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
let rect = this._chart.panes.brush.select('.overlay').node();
let coords = d3.pointers(e, rect)[0];
let xCoord = coords[0];
let item = this._data[this._findIndexForXCoord(xCoord)];
this.fire("mouse_move", { item: item, xCoord: xCoord });
}
};
this._brush
.filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
.on("end.update", brush)
.on("brush.update", focus);
this._chart.panes.brush
.on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
.on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
.on("mousemove.focus touchmove.focus", focus );
},
/**
* Initialize "d3-zoom"
*/
_initClipper() {
let svg = this._container;
let margin = this.options.margins;
const zoom = this._zoom;
const onStart = ({transform, sourceEvent}) => {
if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
if (transform.k == 1 && transform.x == 0) {
this._container.classed('zoomed', true);
// Apply d3-zoom and bind <mask>
if (this._mask) {
this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
}
}
this.zooming = true;
};
const onEnd = ({transform}) => {
if (transform.k ==1 && transform.x == 0) {
this._container.classed('zoomed', false);
// Reset d3-zoom and remove <mask>
if (this._mask) {
this._point.attr('mask', null);
}
}
this.zooming = false;
svg.style('cursor', '');
};
const onZoom = ({transform, sourceEvent}) => {
// TODO: find a faster way to redraw the chart.
this.zooming = false;
this._updateScale(); // hacky way for restoring x scale when zooming out
this.zooming = true;
this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
this._resetDrag();
if (sourceEvent && sourceEvent.type == "mousemove") {
this._hideDiagramIndicator();
}
this.fire('zoom');
};
zoom
.scaleExtent([1, 10])
.extent([
[margin.left, 0],
[this._width() - margin.right, this._height()]
])
.translateExtent([
[margin.left, -Infinity],
[this._width() - margin.right, Infinity]
])
.filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
.on("start", onStart)
.on("end", onEnd)
.on("zoom", onZoom);
svg.call(zoom); // add zoom functionality to "svg" group
// d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
},
_initInteractions() {
this._initBrush();
this._initRuler();
this._initClipper();
this._initLegend();
},
/**
* Toggle chart data on legend click
*/
_initLegend() {
this._container.on('legend_clicked', ({detail}) => {
let { path, legend, name, enabled } = detail;
if (path) {
let label = _.select('text', legend);
let rect = _.select('rect', legend);
_.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
_.toggleStyle(rect, 'fill-opacity', '0', enabled);
_.toggleClass(path, 'leaflet-hidden', enabled);
this._updateArea();
this.fire("elepath_toggle", { path, name, legend, enabled })
}
});
},
/**
* Initialize "ruler".
*/
_initRuler() {
if (!this.options.ruler) return;
// const yMax = this._height();
const formatNum = d3.format(".0f");
const drag = this._drag;
const label = (e, d) => {
let yMax = this._height();
let y = this._ruler.data()[0].y;
if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
this._hideDiagramIndicator();
};
const position = (e, d) => {
let yCoord = d3.pointers(e, this._area.node())[0][1];
let yMax = this._height();
let y = _.clamp(yCoord, [0, yMax]);
this._ruler
.data([L.extend(this._ruler.data()[0], { y: y })])
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.classed('active', y < yMax);
this._container
.select(".horizontal-drag-label")
.text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
}
drag
.on("start end", label)
.on("drag", position);
this._ruler.call(drag);
},
/**
* Initialize x and y scales
*/
_initScale() {
let opts = this.options;
this._registerAxisScale({
axis : 'x',
position: 'bottom',
attr : opts.xAttr,
min : opts.xAxisMin,
max : opts.xAxisMax,
name : 'distance'
});
this._registerAxisScale({
axis : 'y',
position : 'left',
attr : opts.yAttr,
min : opts.yAxisMin,
max : opts.yAxisMax,
name : 'altitude'
});
this._x = this._scales.distance;
this._y = this._scales.altitude;
},
_registerAreaPath(props) {
if (props.scale == 'y') props.scale = this._y;
else if (props.scale == 'x') props.scale = this._x;
let opts = this.options;
if (!props.xAttr) props.xAttr = opts.xAttr;
if (!props.yAttr) props.yAttr = opts.yAttr;
if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
// Save paths in memory for latter usage
this._paths[props.name] = D3.Path(props);
this._props.areas[props.name] = props;
if (opts.legend) {
this._props.legendItems[props.name] = {
name : props.name,
label : props.label,
color : props.color,
className: props.className,
path : this._paths[props.name]
};
}
},
_registerAxisGrid(props) {
if (props.scale == 'y') props.scale = this._y;
else if (props.scale == 'x') props.scale = this._x;
this._props.grids[props.name || props.axis] = props;
},
_registerAxisScale(props) {
if (props.scale == 'y') props.scale = this._y;
else if (props.scale == 'x') props.scale = this._x;
let opts = this.options;
let scale = props.scale;
if (typeof this._scales[props.name] === 'function') {
props.scale = this._scales[props.name]; // retrieve cached scale
} else if (typeof scale !== 'function') {
scale = L.extend({
data : this._data,
forceBounds: opts.forceAxisBounds
}, scale);
scale.attr = scale.attr || props.name;
let domain = this._domains[props.name] = D3.Domain(props);
let range = this._ranges[props.name] = D3.Range(props);
scale.range = scale.range || range(this._width(),this._height());
scale.domain = scale.domain || domain(this._data);
this._props.scales[props.name] = scale;
props.scale = this._scales[props.name] = D3.Scale(scale);
}
if (!props.ticks) {
if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
}
this._props.axes[props.name] = props;
if (props.name == this.options.yScale) this._y = scale;
if (props.name == this.options.xScale) this._x = scale;
return scale;
},
/**
* Add a point of interest over the chart
*/
_registerCheckPoint(point) {
if (!this._data.length) return;
const {xAttr, yAttr} = this.options;
let item, x, y;
if (point.latlng) {
item = this._data[this._findIndexForLatLng(point.latlng)];
x = this._x(item[xAttr]);
y = this._y(item[yAttr]);
} else if (!isNaN(point[xAttr])) {
x = this._x(point[xAttr]);
item = this._data[this._findIndexForXCoord(x)]
y = this._y(item[yAttr]);
}
this._point.call(D3.CheckPoint({
point: point,
width: this._width(),
height: this._height(),
x: x,
y: y,
}));
},
_registerTooltip(props) {
props.order = props.order ?? 1000;
this._props.tooltipItems[props.name] = props;
},
_updateArea() {
// Reset and update chart profiles
this._context.clearRect(0, 0, this._width(), this._height());
_.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
},
_updateAxis() {
let opts = this.options;
const gridOpts = {
width : this._width(),
height : this._height(),
tickFormat: "",
};
const axesOpts = {
width : this._width(),
height : this._height(),
};
// Reset grids
this._grid.selectAll('g').remove();
_.each(this._props.grids, (grid, i) => {
if (opts[i] !== false && opts[i] !== 'summary') {
this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
}
});
// Rest axis
this._axis.selectAll('g').remove();
_.each(this._props.axes, (axis, i) => {
if (opts[i] !== false && opts[i] !== 'summary') {
this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
}
});
// Adjust axis scale positions
this._axis
.selectAll('.y.axis.right')
.each((d, i, n) => {
let axis = d3.select(n[i]);
let transform = axis.attr('transform');
let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
if (i > 0) {
axis.select(':scope > path') .attr('opacity', 0.25);
axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
}
});
},
_updateClipper() {
const { xAttr, margins } = this.options;
const data = this._data;
this._zoom
.scaleExtent([1, 10])
.extent([
[margins.left, 0],
[this._width() - margins.right, this._height()]
])
.translateExtent([
[margins.left, -Infinity],
[this._width() - margins.right, Infinity]
]);
// Apply svg mask on multi-track segments
this._mask.selectAll(".gap").remove()
this._maskGaps.forEach((d, i) => {
if (i >= this._maskGaps.length - 2) return;
let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
this._mask
.append("rect")
.attr("x", x1)
.attr("y", 0)
.attr("width", x2 - x1 )
.attr("height", this._height())
.attr('class', 'gap')
.attr('fill-opacity', '0.0')
.attr("fill", 'black'); // hide = black (mask)
});
},
_updateLegend: function () {
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
// Get legend items
let items = Object.keys(this._paths);
// Calculate legend item positions
let n = items.length;
let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
let rev = v.slice().reverse().map((d) => -(d));
// push a fake element to handle center alignment (odd numbers)
if (n % 2 !== 0) {
rev.push(0);
}
v = rev.concat(v);
// Reset legend items
this._legend.selectAll('g').remove();
// Render legend items
_.each(this._props.legendItems, (legend) => {
this._legend.append("g").call(
D3.LegendItem(L.extend({
width : this._width(),
height : this._height(),
margins: this.options.margins,
}, legend))
);
});
// Render legend item switcher
if (n > 1) {
this._legend.append("g").call(
D3.LegendSmall({
width : this._width(),
height : this._height(),
items : items,
onClick: (selected) => {
_.each(items, name => this._togglePath(name, selected == name, true));
this._updateArea();
}
})
);
}
_.each(items, (name, i) => {
// Adjust legend item positions
this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
// Set initial state (disabled controls)
this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
});
},
_updateMargins() {
// Get chart margins
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
let marginB = 60 + (xAxesB * 2);
let marginL = 10 + (xAxesL * 30);
let marginR = 40 + (xAxesR * 30);
let marginsUpdated = false
// Adjust right margin
if (xAxesR && this.options.margins.right < marginR) {
this.options.margins.right = marginR;
marginsUpdated = true;
}
// Adjust left margin
if (xAxesL && this.options.margins.left < marginL) {
this.options.margins.left = marginL;
marginsUpdated = true;
}
// Adjust bottom margin
if (xAxesB && this.options.margins.bottom < marginB) {
this.options.margins.bottom = marginB;
marginsUpdated = true;
}
if (marginsUpdated) {
this.fire('margins_updated');
}
},
_updateScale() {
if (this.zooming) return { x: this._x, y: this._y };
for (let i in this._scales) {
this._scales[i]
.domain(this._domains[i](this._data))
.range(this._ranges[i](this._width(), this._height()))
}
return { x: this._x, y: this._y };
},
/**
* Calculates chart width.
*/
_width() {
if (this._chart) this._chart._width;
const { width, margins } = this.options;
return width - margins.left - margins.right;
},
/**
* Calculates chart height.
*/
_height() {
if (this._chart) return this._chart._height;
const { height, margins } = this.options;
return height - margins.top - margins.bottom;
},
/*
* Finds data entries above a given y-elevation value and returns geo-coordinates
*/
_findCoordsForY(y) {
let data = this._data;
let z = this._y.invert(y);
// save indexes of elevation values above the horizontal line
const list = data.reduce((array, item, index) => {
if (item[this.options.yAttr] >= z) array.push(index);
return array;
}, []);
let start = 0;
let next;
// split index list into blocks of coordinates
const coords = list.reduce((array, _, curr) => {
next = curr + 1;
if (list[next] !== list[curr] + 1 || next === list.length) {
array.push(
list
.slice(start, next)
.map(i => data[i].latlng)
);
start = next;
}
return array;
}, []);
return coords;
},
/*
* Finds a data entry for a given x-coordinate of the diagram
*/
_findIndexForXCoord(x) {
return d3
.bisector(d => d[this.options.xAttr])
.left(this._data || [0, 1], this._x.invert(x));
},
/*
* Finds a data entry for a given latlng of the map
*/
_findIndexForLatLng(latlng) {
let result = null;
let d = Infinity;
this._data.forEach((item, index) => {
let dist = latlng.distanceTo(item.latlng);
if (dist < d) {
d = dist;
result = index;
}
});
return result;
},
/*
* Removes the drag rectangle and zoms back to the total extent of the data.
*/
_resetDrag() {
if (this._chart.panes.brush.select(".selection").attr('width')) {
this._chart.panes.brush.call(this._brush.clear);
this._hideDiagramIndicator();
this.fire('reset_drag');
}
},
_resetZoom() {
if (this._zoom) {
this._zoom.transform(this._chart.svg, d3.zoomIdentity);
}
},
/**
* Display distance and altitude level ("focus-rect").
*/
_showDiagramIndicator(item, xCoordinate) {
this._tooltip
.attr("display", null)
.call(D3.Tooltip({
xCoord: xCoordinate,
yCoord: this._y(item[this.options.yAttr]),
height: this._height(),
width : this._width(),
labels: this._props.tooltipItems,
item: item
}));
},
_togglePath(name, enabled = true, lazy = false) {
let path = this._paths[name];
let legend = this._container.select('.legend [data-name=' + name + ']');
path.classed('leaflet-hidden', !enabled);
legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
legend.select('rect').style('fill-opacity', enabled ? '': '0');
// Apply d3-zoom (bind <clipPath> mask)
if (this._mask) {
path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
}
if (!lazy) {
this._updateArea();
}
},
_hideDiagramIndicator() {
this._tooltip.attr("display", 'none');
},
/**
* Calculate chart xTicks
*/
_xTicks() {
if (this.__xTicks) this.__xTicks = this.options.xTicks;
return this.__xTicks || Math.round(this._width() / 75);
},
/**
* Calculate chart yTicks
*/
_yTicks() {
if (this.__yTicks) this.__yTicks = this.options.yTicks;
return this.__yTicks || Math.round(this._height() / 30);
}
});

View File

@@ -1,649 +0,0 @@
export const Area = ({
width,
height,
xAttr,
yAttr,
scaleX,
scaleY,
interpolation = "curveLinear"
}) => {
return d3.area()
.curve(typeof interpolation === 'string' ? d3[interpolation] : interpolation)
.x(d => (d.xDiagCoord = scaleX(d[xAttr])))
.y0(height)
.y1(d => scaleY(d[yAttr]));
};
export const Path = ({
name,
color,
strokeColor,
strokeOpacity,
fillOpacity,
className = ''
}) => {
let path = d3.create('svg:path')
if (name) path.classed(name + ' ' + className, true);
path.style("pointer-events", "none");
path
.attr("fill", color || '#3366CC')
.attr("stroke", strokeColor || '#000')
.attr("stroke-opacity", strokeOpacity || '1')
.attr("fill-opacity", fillOpacity || '0.8');
return path;
};
export const Axis = ({
type = "axis",
tickSize = 6,
tickPadding = 3,
position,
height,
width,
axis,
scale,
ticks,
tickFormat,
label,
labelX,
labelY,
name = "",
onAxisMount,
}) => {
return g => {
let [w, h] = [0, 0];
if (position == "bottom") h = height;
if (position == "right") w = width;
if (axis == "x" && type == "grid") {
tickSize = -height;
} else if (axis == "y" && type == "grid") {
tickSize = -width;
}
let axisScale = d3["axis" + position.replace(/\b\w/g, l => l.toUpperCase())]()
.scale(scale)
.ticks(typeof ticks === 'function' ? ticks() : ticks)
.tickPadding(tickPadding)
.tickSize(tickSize)
.tickFormat(tickFormat);
let axisGroup = g.append("g")
.attr("class", [axis, type, position, name].join(" "))
.attr("transform", "translate(" + w + "," + h + ")")
.call(axisScale);
if (label) {
axisGroup.append("svg:text")
.attr("x", labelX)
.attr("y", labelY)
.text(label);
}
if (onAxisMount) {
axisGroup.call(onAxisMount);
}
return axisGroup;
};
};
export const Grid = (props) => {
props.type = "grid";
return Axis(props);
};
export const PositionMarker = ({
theme,
xCoord = 0,
yCoord = 0,
labels = {},
item = {},
length = 0,
}) => {
return g => {
g.attr("class", "height-focus-group");
let line = g.select('.height-focus.line');
let circle = g.select('.height-focus.circle-lower');
let text = g.select('.height-focus-label');
if (!line.node()) line = g.append('svg:line');
if (!circle.node()) circle = g.append('svg:circle');
if (!text.node()) text = g.append('svg:text');
if (isNaN(xCoord) || isNaN(yCoord)) return g;
circle
.attr("class", theme + " height-focus circle-lower")
.attr("transform", "translate(" + xCoord + "," + yCoord + ")")
.attr("r", 6)
.attr("cx", 0)
.attr("cy", 0);
line
.attr("class", theme + " height-focus line")
.attr("x1", xCoord)
.attr("x2", xCoord)
.attr("y1", yCoord)
.attr("y2", length);
text
.attr("class", theme + " height-focus-label")
.style("pointer-events", "none")
.attr("x", xCoord + 5)
.attr("y", length);
let label;
Object
.keys(labels)
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
.forEach((i)=> {
label = text.select(".height-focus-" + labels[i].name);
if (!label.size()) {
label = text.append("svg:tspan")
.attr("class", "height-focus-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
.attr("dy", "1.5em");
}
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
});
text.select('tspan').attr("dy", text.selectAll('tspan').size() > 1 ? "-1.5em" : "0em" );
text.selectAll('tspan').attr("x", xCoord + 5);
return g;
}
};
export const LegendItem = ({
name,
label,
width,
height,
margins = {},
color,
path,
className = ''
}) => {
return g => {
g
.attr("class", "legend-item legend-" + name.toLowerCase())
.attr("data-name", name);
g.on('click.legend', () => d3.select(g.node().ownerSVGElement || g)
.dispatch("legend_clicked", {
detail: {
path: path.node(),
name: name,
legend: g.node(),
enabled: !path.classed('leaflet-hidden'),
}
})
);
g.append("svg:rect")
.attr("x", (width / 2) - 50)
.attr("y", height + margins.bottom / 2)
.attr("width", 50)
.attr("height", 10)
.attr("fill", color)
.attr("stroke", "#000")
.attr("stroke-opacity", "0.5")
.attr("fill-opacity", "0.25")
.attr("class", className);
g.append('svg:text')
.text(L._(label || name))
.attr("x", (width / 2) + 5)
.attr("font-size", 10)
.style("font-weight", "700")
.attr('y', height + margins.bottom / 2)
.attr('dy', "0.75em");
return g;
}
};
export const LegendSmall = ({
width,
height,
items,
onClick
}) => {
return g => {
let idx = 0;
g.data([{
x: width,
y: height + 40
}]).attr("transform", d => "translate(" + d.x + "," + d.y + ")").classed('legend-switcher');
// let label = g.selectAll(".legend-switcher-label") .data([{ idx: 0 }]);
let label = g.append('svg:text')
.attr("class", "legend-switcher-label")
.attr("text-anchor", "end")
.attr("x", -25)
.attr("y", 5)
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
let symbol = g.selectAll(".legend-switcher-symbol").data([
{ type: d3.symbolTriangle, x: 0, y: 3, angle: 0, size: 50, id: "down" },
{ type: d3.symbolTriangle, x: -13, y: 0, angle: 180, size: 50, id: "up" }
]);
symbol.exit().remove();
label.exit().remove();
symbol.enter()
.append("svg:path")
.attr("class", "legend-switcher-symbol")
.attr("cursor", 'pointer')
.attr("fill", "#000")
.merge(symbol)
.attr("d",
d3.symbol()
.type(d => d.type)
.size(d => d.size)
)
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")")
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, items.length])));
const setIdx = (id) => {
idx = id;
// label.enter()
// .append('text')
// .attr("class", "legend-switcher-label")
// .attr("text-anchor", "end")
// .attr("x", -25)
// .attr("y", 5)
// .merge(label.data([{ idx: id }]))
// .text(d => items.length ? (items[idx][0].toUpperCase() + items[idx].slice(1)) : '')
// .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
label.text(items.length ? L._((items[idx][0].toUpperCase() + items[idx].slice(1))) : '');
onClick(items[idx]);
};
setIdx(0);
return g;
};
};
export const Tooltip = ({
xCoord,
yCoord,
width,
height,
labels = {},
item = {},
}) => {
return g => {
let line = g.select('.mouse-focus-line');
let box = g.select('.mouse-focus-label');
if (!line.node()) line = g.append('svg:line');
if (!box.node()) box = g.append("g");
let rect = box.select(".mouse-focus-label-rect");
let text = box.select(".mouse-focus-label-text");
if (!rect.node()) rect = box.append("svg:rect");
if (!text.node()) text = box.append("svg:text");
// Sets focus-label-text position to the left / right of the mouse-focus-line
let xAlign = 0;
let yAlign = 0;
let bbox = { width: 0, height: 0 };
try { bbox = text.node().getBBox(); } catch (e) { return g; }
if (xCoord) xAlign = xCoord + (xCoord < width / 2 ? 10 : -bbox.width - 10);
if (yCoord) yAlign = Math.max(yCoord - bbox.height, L.Browser.webkit ? 0 : -Infinity);
line
.attr('class', 'mouse-focus-line')
.attr('x2', xCoord)
.attr('y2', 0)
.attr('x1', xCoord)
.attr('y1', height);
box
.attr('class', 'mouse-focus-label');
rect
.attr("class", "mouse-focus-label-rect")
.attr("x", xAlign - 5)
.attr("y", yAlign - 5)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 10)
.attr("rx", 3)
.attr("ry", 3);
text
.attr("class", "mouse-focus-label-text")
.style("font-weight", "700")
.attr("y", yAlign);
let label;
Object
.keys(labels)
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
.forEach((i)=> {
label = text.select(".mouse-focus-label-" + labels[i].name);
if (!label.size()) {
label = text.append("svg:tspan", ".mouse-focus-label-x")
.attr("class", "mouse-focus-label-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
.attr("dy", "1.5em");
}
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
});
text.select('tspan').attr("dy", "1em");
text.selectAll('tspan').attr("x", xAlign);
return g;
};
};
export const Ruler = ({
height,
width,
}) => {
return g => {
g.data([{
x: 0,
y: height,
}])
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
let rect = g.selectAll('.horizontal-drag-rect').data([{ w: width }]);
let line = g.selectAll('.horizontal-drag-line').data([{ w: width }]);
let label = g.selectAll('.horizontal-drag-label').data([{ w: width - 8 }]);
let symbol = g.selectAll('.horizontal-drag-symbol')
.data([{
type: d3.symbolTriangle,
x: width + 7,
y: 0,
angle: -90,
size: 50
}]);
rect.exit().remove();
line.exit().remove();
label.exit().remove();
symbol.exit().remove();
rect.enter()
.append("svg:rect")
.attr("class", "horizontal-drag-rect")
.attr("x", 0)
.attr("y", -8)
.attr("height", 8)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.merge(rect)
.attr("width", d => d.w);
line.enter()
.append("svg:line")
.attr("class", "horizontal-drag-line")
.attr("x1", 0)
.merge(line)
.attr("x2", d => d.w);
label.enter()
.append("svg:text")
.attr("class", "horizontal-drag-label")
.attr("text-anchor", "end")
.attr("y", -8)
.merge(label)
.attr("x", d => d.w)
symbol
.enter()
.append("svg:path")
.attr("class", "horizontal-drag-symbol")
.merge(symbol)
.attr("d",
d3.symbol()
.type(d => d.type)
.size(d => d.size)
)
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")");
return g;
}
};
export const CheckPoint = ({
point,
height,
width,
x,
y
}) => {
return g => {
if (isNaN(x) || isNaN(y)) return g;
if (!point.item || !point.item.property('isConnected')) {
point.position = point.position || "bottom";
point.item = g.append('g');
point.item.append("svg:line")
.attr("y1", 0)
.attr("x1", 0)
.attr("style","stroke: rgb(51, 51, 51); stroke-width: 0.5; stroke-dasharray: 2, 2;");
point.item
.append("svg:circle")
.attr("class", " height-focus circle-lower")
.attr("r", 3);
if (point.label) {
point.item.append("svg:text")
.attr("dx", "4px")
.attr("dy", "-4px");
}
}
point.item
.datum({
pos: point.position,
x: x,
y: y
})
.attr("class", d => "point " + d.pos)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
point.item.select('line')
.datum({
y2: ({'top': -y, 'bottom': height - y})[point.position],
x2: ({'left': -x, 'right': width - x})[point.position] || 0
})
.attr("y2", d => d.y2)
.attr("x2", d => d.x2)
if (point.label) {
point.item.select('text')
.text(point.label);
}
return g;
}
};
export const Domain = ({
min,
max,
attr,
name,
forceBounds,
scale
}) => function(data) {
attr = (scale && scale.attr) || attr || name;
let domain = data && data.length ? d3.extent(data, d => d[attr]) : [0, 1];
if (typeof min !== "undefined" && (min < domain[0] || forceBounds)) {
domain[0] = min;
}
if (typeof max !== "undefined" && (max > domain[1] || forceBounds)) {
domain[1] = max;
}
return domain;
};
export const Range = ({
axis
}) => function(width, height) {
if (axis == 'x') return [0, width];
else if (axis == 'y') return [height, 0];
};
export const Scale = ({
data,
attr,
min,
max,
forceBounds,
range,
}) => {
return d3.scaleLinear()
.range(range)
.domain(Domain({min, max, attr, forceBounds})(data));
};
export const Bisect = ({
data = [0, 1],
scale,
x,
attr
}) => {
return d3
.bisector(d => d[attr])
.left(data, scale.invert(x));
};
export const Chart = ({
width,
height,
margins = {},
ruler,
}) => {
const w = width - margins.left - margins.right;
const h = height - margins.top - margins.bottom;
// SVG Container
const svg = d3.create("svg:svg").attr("class", "background");
const defs = svg.append("svg:defs");
// SVG Groups
const g = svg.append("g");
const panes = {
grid : g.append("g").attr("class", "grid"),
area : g.append('g').attr("class", "area"),
axis : g.append('g').attr("class", "axis"),
point : g.append('g').attr("class", "point"),
brush : g.append("g").attr("class", "brush"),
tooltip: g.append("g").attr("class", "tooltip").attr('display', 'none'),
ruler : g.append('g').attr('class', 'ruler'),
legend : g.append('g').attr("class", "legend"),
};
// SVG Paths
const mask = panes.area .append("svg:mask") .attr("id", 'elevation-clipper-' + Math.random().toString(36).substr(2, 9)).attr('fill-opacity', 1);
const maskRect = mask .append("svg:rect") .attr('class', 'zoom') .attr('fill', 'white'); // white = transparent
// Canvas Paths
const foreignObject = panes.area .append('svg:foreignObject').attr('mask', 'url(#' + mask.attr('id') + ')');
const canvas = foreignObject.append('xhtml:canvas') .attr('class', 'canvas-plot');
const context = canvas.node().getContext('2d');
// Add tooltip
panes.tooltip.call(Tooltip({ xCoord: 0, yCoord: 0, height: h, width : w, labels: {} }));
// Add the brushing
let brush = d3.brushX().on('start.cursor end.cursor brush.cursor', () => panes.brush.select(".overlay").attr('cursor', null));
// Scales
const scale = (opts) => ({ x: Scale(opts.x), y: Scale(opts.y)});
let utils = {
defs,
mask,
canvas,
context,
brush,
};
let chart = {
svg,
g,
panes,
utils,
scale,
};
// Resize
chart._resize = ({
width,
height,
margins = {},
ruler,
}) => {
const w = width - margins.left - margins.right;
const h = height - margins.top - margins.bottom;
svg .attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`);
g .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
// Fix: https://github.com/Raruto/leaflet-elevation/issues/123
// Fix: https://github.com/Raruto/leaflet-elevation/issues/232
if (
/Mac|iPod|iPhone|iPad/.test(navigator.platform) &&
/AppleWebkit/i.test(navigator.userAgent) &&
!/Chrome/.test(navigator.userAgent)
) {
canvas .style("transform", "translate(" + margins.left + "px," + margins.top + "px)");
}
maskRect .attr("width", w).attr("height", h).attr("x", 0).attr("y", 0);
foreignObject.attr('width', w).attr('height', h);
canvas .attr('width', w).attr('height', h);
if (ruler) {
panes.ruler.call(Ruler({ height: h, width: w }));
}
panes.brush.call(brush.extent( [ [0,0], [w, h] ] ));
panes.brush.select(".overlay").attr('cursor', null);
chart._width = w;
chart._height = h;
chart.svg.dispatch('resize', { detail: { width: w, height: h } } );
};
chart.pane = (name) => (panes[name] || (panes[name] = g.append('g').attr("class", name)));
chart.get = (name) => utils[name];
chart._resize({ width, height, margins});
return chart;
};

View File

@@ -1,96 +0,0 @@
import * as D3 from './d3.js';
const _ = L.Control.Elevation.Utils;
export var Marker = L.Class.extend({
initialize(options, control) {
this.options = options;
this.control = control
switch(this.options.marker) {
case 'elevation-line':
// this._container = d3.create("g").attr("class", "height-focus-group");
break;
case 'position-marker':
// this._marker = L.circleMarker([0, 0], { pane: 'overlayPane', radius: 6, fillColor: '#fff', fillOpacity:1, color: '#000', weight:1, interactive: false });
this._marker = L.marker([0, 0], { icon: this.options.markerIcon, zIndexOffset: 1000000, interactive: false });
break;
}
this._labels = {};
return this;
},
addTo(map) {
this._map = map;
switch(this.options.marker) {
case 'elevation-line': this._container = d3.select(map.getPane('elevationPane')).select("svg > g").call(D3.PositionMarker({})); break;
case 'position-marker': this._marker.addTo(map, { pane: 'overlayPane' }); break;
}
return this;
},
/**
* Update position marker ("leaflet-marker").
*/
update(props) {
if (props) this._props = props;
else props = this._props;
if (!props) return;
if (props.options) this.options = props.options;
if (!this._map) this.addTo(props.map);
this._latlng = props.item.latlng;
switch(this.options.marker) {
case 'elevation-line':
if (this._container) {
let point = this._map.latLngToLayerPoint(this._latlng);
point = L.extend({}, props.item, this._map._rotate ? this._map.rotatedPointToMapPanePoint(point) : point);
let yMax = (this.control._height() / props.yCoordMax * point[this.options.yAttr]);
if (!isFinite(yMax) || isNaN(yMax)) yMax = 0;
this._container.classed("leaflet-hidden", false);
this._container.call(D3.PositionMarker({
theme : this.options.theme,
xCoord: point.x,
yCoord: point.y,
length: point.y - yMax, // normalized Y
labels: this._labels,
item: point,
}));
}
break;
case 'position-marker':
_.removeClass(this._marker.getElement(), 'leaflet-hidden');
this._marker.setLatLng(this._latlng);
break;
}
},
/*
* Hides the position/height indicator marker drawn onto the map
*/
remove() {
this._props = null;
switch(this.options.marker) {
case 'elevation-line': this._container && this._container.classed("leaflet-hidden", true); break;
case 'position-marker': _.addClass(this._marker.getElement(), 'leaflet-hidden'); break;
}
},
getLatLng() {
return this._latlng;
},
_registerTooltip(props) {
this._labels[props.name] = props;
}
});

View File

@@ -1,43 +0,0 @@
const _ = L.Control.Elevation.Utils;
export var Summary = L.Class.extend({
initialize(opts, control) {
this.options = opts;
this.control = control;
this.labels = {};
let summary = this._container = _.create("div", "elevation-summary " + (opts.summary ? opts.summary + "-summary" : ''));
_.style(summary, 'max-width', opts.width ? opts.width + 'px' : '');
},
render() {
return container => container.append(() => this._container);
},
reset() {
this._container.innerHTML = '';
},
append(className, label, value) {
this._container.innerHTML += `<span class="${className}"><span class="summarylabel">${label}</span><span class="summaryvalue">${value}</span></span>`;
return this;
},
update() {
Object
.keys(this.labels)
.sort((a, b) => this.labels[a].order - this.labels[b].order) // TODO: any performance issues?
.forEach((i)=> {
let value = typeof this.labels[i].value !== "function" ? this.labels[i].value : this.labels[i].value(this.control.track_info, this.labels[i].unit || '');
this.append(i /*+ " order-" + this.labels[i].order*/, L._(this.labels[i].label), value, this.labels[i].order);
});
},
_registerSummary(data) {
for (let i in data) {
data[i].order = data[i].order ?? 1000;
this.labels[i] = data[i];
}
}
});

View File

@@ -1,64 +0,0 @@
export function Acceleration() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let acceleration = {};
acceleration.label = opts.accelerationLabel || L._(opts.imperial ? 'ft/s²' : 'm/s²');
opts.accelerationFactor = opts.accelerationFactor || 1;
return {
name: 'acceleration',
unit: acceleration.label,
deltaMax: this.options.accelerationDeltaMax,
clampRange: this.options.accelerationRange,
decimals: 2,
pointToAttr: (_, i) => {
let dv = (this._data[i].speed - this._data[i > 0 ? i - 1 : i].speed) * (1000 / opts.timeFactor);
let dt = (this._data[i].time - this._data[i > 0 ? i - 1 : i].time) / 1000;
return dt > 0 ? Math.abs((dv / dt)) * opts.accelerationFactor : NaN;
},
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale: {
axis : "y",
position : "right",
scale : { min: 0, max: +1 },
tickPadding: 16,
labelX : 25,
labelY : -8,
},
path: {
label : 'Acceleration',
yAttr : 'acceleration',
scaleX : 'distance',
scaleY : 'acceleration',
color : '#050402',
strokeColor : '#000',
strokeOpacity: "0.5",
fillOpacity : "0.25",
},
tooltip: {
chart: (item) => L._("a: ") + item.acceleration + " " + acceleration.label,
marker: (item) => Math.round(item.acceleration) + " " + acceleration.label,
order: 60,
},
summary: {
"minacceleration" : {
label: "Min Acceleration: ",
value: (track, unit) => Math.round(track.acceleration_min || 0) + '&nbsp;' + unit,
order: 60
},
"maxacceleration" : {
label: "Max Acceleration: ",
value: (track, unit) => Math.round(track.acceleration_max || 0) + '&nbsp;' + unit,
order: 61
},
"avgacceleration": {
label: "Avg Acceleration: ",
value: (track, unit) => Math.round(track.acceleration_avg || 0) + '&nbsp;' + unit,
order: 62
},
}
};
}

View File

@@ -1,78 +0,0 @@
export function Altitude() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let altitude = {};
let theme = opts.theme.split(' ')[0].replace('-theme', '');
let color = _.Colors[theme] || {};
opts.altitudeFactor = opts.imperial ? this.__footFactor : (opts.altitudeFactor || 1); // 1 m = (1 m)
altitude.label = opts.imperial ? "ft" : opts.yLabel;
return {
name: 'altitude',
required: this.options.slope,
meta: 'z',
unit: altitude.label,
statsName: 'altitude',
deltaMax: this.options.altitudeDeltaMax,
clampRange: this.options.altitudeRange,
// init: ({point}) => {
// // "alt" property is generated inside "leaflet"
// if ("alt" in point) point.meta.ele = point.alt;
// },
pointToAttr: (point, i) => {
if ("alt" in point) point.meta.ele = point.alt; // "alt" property is generated inside "leaflet"
return this._data[i].z *= opts.altitudeFactor;
},
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
grid: {
axis : "y",
position : "left",
scale : "y" // this._chart._y,
},
scale: {
axis : "y",
position: "left",
scale : "y", // this._chart._y,
labelX : -3,
labelY : -8,
},
path: {
label : 'Altitude',
scaleX : 'distance',
scaleY : 'altitude',
className : 'area',
color : color.area || theme,
strokeColor : opts.detached ? color.stroke : '#000',
strokeOpacity: "1",
fillOpacity : opts.detached ? (color.alpha || '0.8') : 1,
preferCanvas : opts.preferCanvas,
},
tooltip: {
name: 'y',
chart: (item) => L._("y: ") + _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
marker: (item) => _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
order: 10,
},
summary: {
"minele" : {
label: "Min Elevation: ",
value: (track, unit) => (track.elevation_min || 0).toFixed(2) + '&nbsp;' + unit,
order: 30,
},
"maxele" : {
label: "Max Elevation: ",
value: (track, unit) => (track.elevation_max || 0).toFixed(2) + '&nbsp;' + unit,
order: 31,
},
"avgele" : {
label: "Avg Elevation: ",
value: (track, unit) => (track.elevation_avg || 0).toFixed(2) + '&nbsp;' + unit,
order: 32,
},
}
};
}

View File

@@ -1,54 +0,0 @@
export function Cadence() {
const _ = L.Control.Elevation.Utils;
return {
name: 'cadence', // <-- Your custom option name (eg. "cadence: true")
unit: 'rpm',
meta: 'cad', // <-- point.meta.cad
coordinateProperties: ["cads", "cadences", "cad", "cadence"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
pointToAttr: (point, i) => (point.cad ?? point.meta.cad ?? point.prev('cadence')) || 0,
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale: {
axis : "y",
position : "right",
scale : { min: -1, max: +1 },
tickPadding: 16,
labelX : 25,
labelY : -8,
},
path: {
label : 'RPM',
yAttr : 'cadence',
scaleX : 'distance',
scaleY : 'cadence',
color : '#FFF',
strokeColor : 'blue',
strokeOpacity: "0.85",
fillOpacity : "0.1",
},
tooltip: {
name: 'cadence',
chart: (item) => L._("cad: ") + item.cadence + " " + 'rpm',
marker: (item) => Math.round(item.cadence) + " " + 'rpm',
order: 1
},
summary: {
"minrpm": {
label: "Min RPM: ",
value: (track, unit) => Math.round(track.cadence_min || 0) + '&nbsp;' + unit,
// order: 30
},
"maxrpm": {
label: "Max RPM: ",
value: (track, unit) => Math.round(track.cadence_max || 0) + '&nbsp;' + unit,
// order: 30
},
"avgrpm": {
label: "Avg RPM: ",
value: (track, unit) => Math.round(track.cadence_avg || 0) + '&nbsp;' + unit,
// order: 20
},
}
};
}

View File

@@ -1,46 +0,0 @@
export function Distance() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let distance = {};
opts.distanceFactor = opts.imperial ? this.__mileFactor : (opts.distanceFactor || 1); // 1 km = (1000 m)
distance.label = opts.imperial ? "mi" : opts.xLabel;
return {
name: 'distance',
required: true,
attr: 'dist',
unit: distance.label,
decimals: 5,
pointToAttr: (_, i) => (i > 0 ? this._data[i - 1].dist : 0) + (this._data[i].latlng.distanceTo(this._data[i > 0 ? i - 1 : i].latlng) * opts.distanceFactor) / 1000, // convert back km to meters
// stats: { total: _.iSum },
onPointAdded: (distance, i) => this.track_info.distance = distance,
scale: opts.distance && {
axis : "x",
position: "bottom",
scale : "x", // this._chart._x,
labelY : 25,
labelX : () => this._width() + 6,
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
},
grid: opts.distance && {
axis : "x",
position : "bottom",
scale : "x" // this._chart._x,
},
tooltip: opts.distance && {
name: 'x',
chart: (item) => L._("x: ") + _.round(item[opts.xAttr], opts.decimalsX) + " " + distance.label,
order: 20
},
summary: opts.distance && {
"totlen" : {
label: "Total Length: ",
value: (track) => (track.distance || 0).toFixed(2) + '&nbsp;' + distance.label,
order: 10
}
}
};
}

View File

@@ -1,53 +0,0 @@
export function Heart() {
const _ = L.Control.Elevation.Utils;
return {
name: 'heart', // <-- Your custom option name (eg. "heart: true")
unit: 'bpm',
meta: 'hr', // <-- point.meta.hr
coordinateProperties: ["heart", "heartRates", "heartRate"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
pointToAttr: (point, i) => (point.hr ?? point.meta.hr ?? point.prev('heart')) || 0,
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale: {
axis : "y",
position : "left",
scale : { min: -1, max: +1 },
tickPadding: 25,
labelX : -30,
labelY : -8,
},
path: {
label : 'ECG',
yAttr : 'heart',
scaleX : 'distance',
scaleY : 'heart',
color : 'white',
strokeColor : 'red',
strokeOpacity: "0.85",
fillOpacity : "0.1",
},
tooltip: {
chart: (item) => L._("hr: ") + item.heart + " " + 'bpm',
marker: (item) => Math.round(item.heart) + " " + 'bpm',
order: 1
},
summary: {
"minbpm": {
label: "Min BPM: ",
value: (track, unit) => Math.round(track.heart_min || 0) + '&nbsp;' + unit,
// order: 30
},
"maxbpm": {
label: "Max BPM: ",
value: (track, unit) => Math.round(track.heart_max || 0) + '&nbsp;' + unit,
// order: 30
},
"avgbpm": {
label: "Avg BPM: ",
value: (track, unit) => Math.round(track.heart_avg || 0) + '&nbsp;' + unit,
// order: 20
},
}
};
};

View File

@@ -1,45 +0,0 @@
/**
* @see https://github.com/Raruto/leaflet-elevation/issues/211
*
* @example
* ```js
* L.control.Elevation({ handlers: [ 'Labels' ], labelsRotation: 25, labelsAlign: 'start' })
* ```
*/
export function Labels() {
this.on('elechart_updated', function(e) {
const pointG = this._chart._chart.pane('point');
const textRotation = this.options.labelsRotation ?? -90;
const textAnchor = this.options.labelsAlign;
if (90 == Math.abs(textRotation)) {
pointG.selectAll('text')
.attr('dy', '4px')
.attr("dx", (d, i, el) => Math.sign(textRotation) * (this._height() - d3.select(el[i].parentElement).datum().y - 8) + 'px')
.attr('text-anchor', textRotation > 0 ? 'end' : 'start')
.attr('transform', 'rotate(' + textRotation + ')')
pointG.selectAll('circle')
.attr('r', '2.5')
.attr('fill', '#fff')
.attr("stroke", '#000')
.attr("stroke-width", '1.1');
} else if (!isNaN(textRotation)) {
pointG.selectAll('text')
.attr("dx", "4px")
.attr("dy", "-9px")
.attr('text-anchor', textAnchor ?? (0 == textRotation ? 'start' : 'middle'))
.attr('transform', 'rotate('+ -textRotation +')')
}
});
return { };
}

View File

@@ -1,122 +0,0 @@
/**
* @see https://github.com/Raruto/leaflet-elevation/issues/251
*
* @example
* ```js
* L.control.Elevation({
* altitude: true,
* distance: true,
* handlers: [ 'Altitude', 'Distance', 'LinearGradient', ],
* linearGradient: {
* attr: 'z',
* path: 'altitude',
* range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
* min: 'elevation_min',
* max: 'elevation_max',
* },
* })
* ```
*/
export function LinearGradient() {
if (!this.options.linearGradient) {
return {};
}
const _ = L.Control.Elevation.Utils;
/**
* Initialize gradient color palette.
*/
const get_palette = function ({range, min, max, depth = 256}) {
const canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
gradient = ctx.createLinearGradient(0, 0, 0, depth);
canvas.width = 1;
canvas.height = depth;
for (let i in range) {
gradient.addColorStop(i, range[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, depth);
const { data } = ctx.getImageData(0, 0, 1, depth);
return {
/**
* Gets the RGB values of a given z value of the current palette.
*
* @param {number} value - Value to get the color for, should be between min and max.
* @returns {string} The RGB values as `rgb(r, g, b)` string
*/
getRGBColor(value) {
const idx = Math.floor(Math.min(Math.max((value - min) / (max - min), 0), 0.999) * depth) * 4;
return 'rgb(' + [data[idx], data[idx + 1], data[idx + 2]].join(',') + ')';
}
};
};
const { preferCanvas } = this.options;
const { attr, path: path_name, range, min, max } = L.extend({
attr: 'z',
path: 'altitude',
range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
min: 'elevation_min',
max: 'elevation_max',
}, (true === this.options.linearGradient) ? {} : this.options.linearGradient);
const gradient_id = path_name + '-gradient-' + _.randomId();
const legend_id = 'legend-' + gradient_id;
// Charte profile gradient
this.on('elechart_axis', () => {
if (!this._data.length) return;
const chart = this._chart;
const path = chart._paths[path_name];
const { defs } = chart._chart.utils;
const palette = get_palette({
min: isFinite(this.track_info[min]) ? this.track_info[min] : 0,
max: isFinite(this.track_info[max]) ? this.track_info[max] : 1,
range,
});
let gradient;
if (preferCanvas) {
/** ref: `path.__fillStyle` within L.Control.Elevation.Utils::drawCanvas(ctx, path) */
path.__fillStyle = gradient = chart._context.createLinearGradient(0, 0, chart._width(), 0);
} else {
defs.select('#' + gradient_id).remove();
gradient = defs.append('svg:linearGradient').attr('id', gradient_id);
gradient.addColorStop = function(offset, color) { gradient.append('svg:stop').attr('offset', offset).attr('stop-color', color) };
path.attr('fill', 'url(#' + gradient_id + ')').classed('area', false);
}
// Generate gradient for each segment picking colors from palette
for (let i = 0, data = this._data; i < data.length; i++) {
gradient.addColorStop((i) / data.length, palette.getRGBColor(data[i][attr]));
}
});
// Legend item gradient
this.on('elechart_updated', () => {
const chart = this._chart;
const { defs } = chart._chart.utils;
defs.select('#' + legend_id).remove();
const legendGradient = defs.append('svg:linearGradient').attr('id', legend_id);
Object.keys(range).sort().forEach(i => legendGradient.append('svg:stop').attr('offset', i).attr('stop-color', range[i]));
chart._container
.select('.legend-' + path_name + ' > rect')
.attr('fill', 'url(#' + legend_id + ')')
.classed('area', false);
});
return { };
}

View File

@@ -1,65 +0,0 @@
export function Pace() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let pace = {};
pace.label = opts.paceLabel || L._(opts.imperial ? 'min/mi' : 'min/km');
opts.paceFactor = opts.paceFactor || 60; // 1 min = 60 sec
return {
name: 'pace',
unit: pace.label,
deltaMax: this.options.paceDeltaMax,
clampRange: this.options.paceRange,
decimals: 2,
pointToAttr: (_, i) => {
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
return dx > 0 ? Math.abs((dt / dx) / opts.paceFactor) : NaN;
},
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale : (this.options.pace && this.options.pace != "summary") && {
axis : "y",
position : "right",
scale : { min : 0, max : +1 },
tickPadding: 16,
labelX : 25,
labelY : -8,
},
path: (this.options.pace && this.options.pace != "summary") && {
// name : 'pace',
label : 'Pace',
yAttr : "pace",
scaleX : 'distance',
scaleY : 'pace',
color : '#03ffff',
strokeColor : '#000',
strokeOpacity: "0.5",
fillOpacity : "0.25",
},
tooltip: (this.options.pace) && {
chart: (item) => L._('pace: ') + item.pace + " " + pace.label,
marker: (item) => Math.round(item.pace) + " " + pace.label,
order: 50,
},
summary: (this.options.pace) && {
"minpace" : {
label: "Min Pace: ",
value: (track, unit) => Math.round(track.pace_min || 0) + '&nbsp;' + unit,
order: 51
},
"maxpace" : {
label: "Max Pace: ",
value: (track, unit) => Math.round(track.pace_max || 0) + '&nbsp;' + unit,
order: 51
},
"avgpace": {
label: "Avg Pace: ",
value: (track, unit) => Math.round(track.pace_avg || 0) + '&nbsp;' + unit,
order: 52
},
}
};
}

View File

@@ -1,109 +0,0 @@
/**
* @see https://github.com/Igor-Vladyka/leaflet.motion
*
* @example
* ```js
* L.control.Elevation({ handlers: [ 'Runner' ], runnerOptions: { polyline: {..}, motion: {..}, marker {..} } })
* ```
*/
export async function Runner() {
await this.import(this.__LMOTION || 'https://unpkg.com/leaflet.motion@0.3.2/dist/leaflet.motion.min.js', typeof L.Motion !== 'object')
let { runnerOptions } = this.options;
runnerOptions = L.extend(
{ polyline: {}, motion: {}, marker: undefined },
'object' === typeof runnerOptions ? runnerOptions : {}
);
// Custom tooltips
this._registerTooltip({
name: 'here',
marker: (item) => L._("You are here: "),
order: 1,
});
this._registerTooltip({
name: 'distance',
marker: (item) => Math.round(item.dist) + " " + this.options.xLabel,
order: 2,
});
this.addCheckpoint = function (checkpoint) {
return this._registerCheckPoint({ // <-- NB these are private functions use them at your own risk!
latlng: this._findItemForX(this._x(checkpoint.dist)).latlng,
label: checkpoint.label || ''
});
}
this.addRunner = function (runner) {
let x = this._x(runner.dist);
let y = this._y(this._findItemForX(x).z)
let g = d3.select(this._container)
.select('svg > g')
.append("svg:circle")
.attr("class", "runner " + this.options.theme + " height-focus circle-lower")
.attr("r", 6)
.attr("cx", x)
.attr("cy", y);
return g;
}
this.setPositionFromLatLng = function (latlng) {
this._onMouseMoveLayer({ latlng: latlng }); // Update map and chart "markers" from latlng
};
this.tick = function (runner, dist = 0, inc = 0.1) {
dist = (dist <= this.track_info.distance - inc) ? dist + inc : 0;
this.updateRunnerPos(runner, dist);
setTimeout(() => this.tick(runner, dist), 150);
};
this.updateRunnerPos = function (runner, pos) {
let curr, x;
if (pos instanceof L.LatLng) {
curr = this._findItemForLatLng(pos);
x = this._x(curr.dist);
} else {
x = this._x(pos);
curr = this._findItemForX(x);
}
runner
.attr("cx", x)
.attr("cy", this._y(curr.z));
this.setPositionFromLatLng(curr.latlng);
};
this.animate = function (layer, speed = 1500) {
if (this._runner) {
this._runner.remove();
}
layer.setStyle({ opacity: 0.5 });
const geo = L.geoJson(layer.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1)) });
this._runner = L.motion.polyline(
geo.toGeoJSON().features[0].geometry.coordinates,
L.extend({}, { color: 'red', pane: 'elevationPane', attribution: '' }, runnerOptions.polyline),
L.extend({}, { auto: true, speed: speed, }, runnerOptions.motion),
runnerOptions.marker || undefined
);
// Override default function behavior: `L.Motion.Polyline::_drawMarker()`
this._runner._drawMarker = new Proxy(this._runner._drawMarker, {
apply: (target, thisArg, argArray) => {
thisArg._runner = thisArg._runner || this.addRunner({ dist: 0 });
this.updateRunnerPos(thisArg._runner, argArray[0]);
return target.apply(thisArg, argArray);
}
});
this._runner.addTo(this._map);
};
return {};
}

View File

@@ -1,79 +0,0 @@
export function Slope() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let slope = {};
slope.label = opts.slopeLabel || '%';
return {
name: 'slope',
meta: 'slope',
unit: slope.label,
deltaMax: this.options.slopedDeltaMax,
clampRange: this.options.slopeRange,
decimals: 2,
pointToAttr: (_, i) => { // slope in % = ( dy / dx ) * 100;
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
let dy = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
return dx !== 0 ? (dy / dx) * 100 : NaN;
},
onPointAdded: (_, i) => {
let dz = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
if (dz > 0) this.track_info.ascent = (this.track_info.ascent || 0) + dz; // Total Ascent
else if (dz < 0) this.track_info.descent = (this.track_info.descent || 0) - dz; // Total Descent
},
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg, },
scale: {
axis : "y",
position : "right",
scale : { min: -1, max: +1 },
tickPadding: 16,
labelX : 25,
labelY : -8,
},
path: {
label : 'Slope',
yAttr : 'slope',
scaleX : 'distance',
scaleY : 'slope',
color : '#F00',
strokeColor : '#000',
strokeOpacity: "0.5",
fillOpacity : "0.25",
},
tooltip: {
chart: (item) => L._("m: ") + item.slope + slope.label,
marker: (item) => Math.round(item.slope) + slope.label,
order: 40,
},
summary: {
"minslope": {
label: "Min Slope: ",
value: (track, unit) => Math.round(track.slope_min || 0) + '&nbsp;' + unit,
order: 40
},
"maxslope": {
label: "Max Slope: ",
value: (track, unit) => Math.round(track.slope_max || 0) + '&nbsp;' + unit,
order: 41
},
"avgslope": {
label: "Avg Slope: ",
value: (track, unit) => Math.round(track.slope_avg || 0) + '&nbsp;' + unit,
order: 42
},
"ascent" : {
label: "Total Ascent: ",
value: (track, unit) => Math.round(track.ascent || 0) + '&nbsp;' + (this.options.imperial ? 'ft' : 'm'),
order: 43
},
"descent" : {
label: "Total Descent: ",
value: (track, unit) => Math.round(track.descent || 0) + '&nbsp;' + (this.options.imperial ? 'ft' : 'm'),
order: 45
},
}
};
}

View File

@@ -1,66 +0,0 @@
export function Speed() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let speed = {};
speed.label = opts.speedLabel || L._(opts.imperial ? 'mph' : 'km/h');
opts.speedFactor = opts.speedFactor || 1;
return {
name: 'speed',
required: (this.options.acceleration),
unit: speed.label,
deltaMax: this.options.speedDeltaMax,
clampRange: this.options.speedRange,
decimals: 2,
pointToAttr: (_, i) => {
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
return dt > 0 ? Math.abs((dx / dt) * opts.timeFactor) * opts.speedFactor : NaN;
},
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale : (this.options.speed && this.options.speed != "summary") && {
axis : "y",
position : "right",
scale : { min : 0, max : +1 },
tickPadding: 16,
labelX : 25,
labelY : -8,
},
path: (this.options.speed && this.options.speed != "summary") && {
// name : 'speed',
label : 'Speed',
yAttr : "speed",
scaleX : 'distance',
scaleY : 'speed',
color : '#03ffff',
strokeColor : '#000',
strokeOpacity: "0.5",
fillOpacity : "0.25",
},
tooltip: (this.options.speed) && {
chart: (item) => L._('v: ') + item.speed + " " + speed.label,
marker: (item) => Math.round(item.speed) + " " + speed.label,
order: 50,
},
summary: (this.options.speed) && {
"minspeed" : {
label: "Min Speed: ",
value: (track, unit) => Math.round(track.speed_min || 0) + '&nbsp;' + unit,
order: 51
},
"maxspeed" : {
label: "Max Speed: ",
value: (track, unit) => Math.round(track.speed_max || 0) + '&nbsp;' + unit,
order: 51
},
"avgspeed": {
label: "Avg Speed: ",
value: (track, unit) => Math.round(track.speed_avg || 0) + '&nbsp;' + unit,
order: 52
},
}
};
}

View File

@@ -1,62 +0,0 @@
export function Temperature() {
const _ = L.Control.Elevation.Utils;
let temperature = {};
let opts = this.options;
temperature.label = opts.label || L._(opts.imperial ? '°F' : '°C');
// Fahrenheit = (Celsius * 9/5) + 32
opts.temperatureFactor1 = opts.temperatureFactor1 ?? (opts.imperial ? 1.8 : 1);
opts.temperatureFactor2 = opts.temperatureFactor2 ?? (opts.imperial ? 32 : 0);
return {
name: 'temperature', // <-- Your custom option name (eg. "temperature: true")
unit: temperature.label,
meta: 'atemps', // <-- point.meta.atemps
coordinateProperties: ["atemps"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
deltaMax: this.options.temperatureDeltaMax,
clampRange: this.options.temperatureRange,
decimals: 2,
pointToAttr: (point, i) => (point.meta.atemps ?? point.meta.atemps ?? point.prev('temperature')) * opts.temperatureFactor1 + opts.temperatureFactor2,
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
scale: {
axis : "y",
position : "right",
scale : { min: -1, max: +1 },
tickPadding: 16,
labelX : +18,
labelY : -8,
},
path: {
label : temperature.label,
yAttr : 'temperature',
scaleX : 'distance',
scaleY : 'temperature',
color : 'transparent',
strokeColor : '#000',
strokeOpacity: "0.85",
// fillOpacity : "0.1",
},
tooltip: {
name: 'temperature',
chart: (item) => L._("Temp: ") + Math.round(item.temperature).toLocaleString() + " " + temperature.label,
marker: (item) => Math.round(item.temperature).toLocaleString() + " " + temperature.label,
order: 1
},
summary: {
"mintemp": {
label: "Min Temp: ",
value: (track, unit) => Math.round(track.temperature_min || 0) + '&nbsp;' + unit,
},
"maxtemp": {
label: "Max Temp: ",
value: (track, unit) => Math.round(track.temperature_max || 0) + '&nbsp;' + unit,
},
"avgtemp": {
label: "Avg Temp: ",
value: (track, unit) => Math.round(track.temperature_avg || 0) + '&nbsp;' + unit,
},
}
};
}

View File

@@ -1,103 +0,0 @@
export function Time() {
const _ = L.Control.Elevation.Utils;
let opts = this.options;
let time = {};
time.label = opts.timeLabel || 't';
opts.timeFactor = opts.timeFactor || 3600;
/**
* Common AVG speeds:
* ----------------------
* slow walk = 1.8 km/h
* walking = 3.6 km/h <-- default: 3.6
* running = 10.8 km/h
* cycling = 18 km/h
* driving = 72 km/h
* ----------------------
*/
this._timeAVGSpeed = (opts.timeAVGSpeed || 3.6) * (opts.speedFactor || 1);
if (!opts.timeFormat) {
opts.timeFormat = (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
} else if (opts.timeFormat == 'time') {
opts.timeFormat = (time) => (new Date(time)).toLocaleTimeString();
} else if (opts.timeFormat == 'date') {
opts.timeFormat = (time) => (new Date(time)).toLocaleDateString();
}
opts.xTimeFormat = opts.xTimeFormat || ((t) => _.formatTime(t).split("'")[0]);
return {
name: 'time',
required: (this.options.speed || this.options.acceleration || this.options.timestamps),
coordinateProperties: ["coordTimes", "times", "time"],
coordPropsToMeta: _.parseDate,
pointToAttr: function(point, i) {
// Add missing timestamps (see: options.timeAVGSpeed)
if (!point.meta || !point.meta.time) {
point.meta = point.meta || {};
if (i > 0) {
let dx = (this._data[i].dist - this._data[i - 1].dist);
let t0 = this._data[i - 1].time.getTime();
point.meta.time = new Date(t0 + ( dx / this._timeAVGSpeed) * this.options.timeFactor * 1000);
} else {
point.meta.time = new Date(Date.now())
}
}
// Handle timezone offset
let time = (point.meta.time.getTime() - point.meta.time.getTimezoneOffset() * 60 * 1000 !== 0) ? point.meta.time : 0;
// Update duration
this._data[i].duration = i > 0 ? (this._data[i - 1].duration || 0) + Math.abs(time - this._data[i - 1].time) : 0;
return time;
},
onPointAdded: (_, i) => this.track_info.time = this._data[i].duration,
scale: (opts.time && opts.time != "summary" && !L.Browser.mobile) && {
axis : "x",
position : "top",
scale : {
attr : "duration",
min : 0,
},
label : time.label,
labelY : -10,
labelX : () => this._width(),
name : "time",
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
tickFormat : (d) => (d == 0 ? '' : opts.xTimeFormat(d)),
onAxisMount: axis => {
axis.select(".domain")
.remove();
axis.selectAll("text")
.attr('opacity', 0.65)
.style('font-family', 'Monospace')
.style('font-size', '110%');
axis.selectAll(".tick line")
.attr('y2', this._height())
.attr('stroke-dasharray', 2)
.attr('opacity', 0.75);
}
},
tooltips: [
(this.options.time) && {
name: 'time',
chart: (item) => L._("T: ") + _.formatTime(item.duration || 0),
order: 20
},
(this.options.timestamps) && {
name: 'date',
chart: (item) => L._("t: ") + this.options.timeFormat(item.time),
order: 21,
}
],
summary: (this.options.time) && {
"tottime" : {
label: "Total Time: ",
value: (track) => _.formatTime(track.time || 0),
order: 20
}
}
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB