strava-frontend/src/pages/routes/Route.vue

196 lines
6.1 KiB
Vue

<template>
<section class="map">
<div id="workout-map">
<yandex-map
v-model="map"
:settings="{
location: {
center: mapCenter,
zoom: 10,
},
}"
width="100%"
:height="height"
>
<yandex-map-default-scheme-layer />
<yandex-map-default-features-layer />
<yandex-map-listener
:settings="{
onClick: (_: any, e: any) => handleMapClick(e.coordinates),
}"
/>
<yandex-map-default-marker
v-if="clickCoordinates.length === 1"
:settings="{
title: 'Начальная точка',
coordinates: clickCoordinates[0],
}"
/>
<yandex-map-feature
:settings="{
geometry: {
type: 'LineString',
coordinates: clickCoordinates,
},
style: {
stroke: [{ color: '#007afce6', width: 4 }],
},
}"
/>
<yandex-map-controls
:settings="{ position: 'right top', orientation: 'vertical' }"
>
<yandex-map-control>
<div class="info" v-if="clickCoordinates.length <= 1">
Вы можете добавлять новые точки<br />
на карту путём клика на неё
</div>
<div class="info" v-if="clickCoordinates.length > 1">
Отмеченная дистанция: {{ calculateTotalDistance() }} км
</div>
</yandex-map-control>
<yandex-map-control-button
v-if="clickCoordinates.length"
:settings="{
background: 'blue',
color: '#fff',
onClick: () => downloadAsGPX(),
}"
>
Выгрузить
</yandex-map-control-button>
<yandex-map-control-button
v-if="clickCoordinates.length"
:settings="{
background: 'blue',
color: '#fff',
onClick: () => clickCoordinates.pop(),
}"
>
Отменить последнюю точку
</yandex-map-control-button>
<yandex-map-control-button
v-if="clickCoordinates.length"
:settings="{
background: '#fd6466e6',
color: '#fff',
onClick: () => (clickCoordinates = []),
}"
>
Стереть точки ({{ clickCoordinates.length }})
</yandex-map-control-button>
</yandex-map-controls>
</yandex-map>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, shallowRef } from "vue";
import type { LngLat, YMap } from "@yandex/ymaps3-types";
import {
YandexMap,
YandexMapDefaultSchemeLayer,
YandexMapFeature,
YandexMapDefaultFeaturesLayer,
YandexMapDefaultMarker,
YandexMapControl,
YandexMapControlButton,
YandexMapControls,
YandexMapListener,
} from "vue-yandex-maps";
const mapCenter = ref<LngLat>([30.31413, 59.93863]);
const height = `${window.innerHeight}px`;
const map = shallowRef<null | YMap>(null);
const clickCoordinates = ref<LngLat[]>([]);
const MIN_DISTANCE_M = 5;
function handleMapClick(coords: LngLat): void {
if (clickCoordinates.value.length > 0) {
const last = clickCoordinates.value[clickCoordinates.value.length - 1];
if (
calculateHaversineDistance(coords[0], coords[1], last[0], last[1]) *
1000 <
MIN_DISTANCE_M
) {
return;
}
}
clickCoordinates.value = [...clickCoordinates.value, coords];
}
const toGPX = () => {
let content = "";
const date = new Date().toISOString();
for (let i = 0; i < clickCoordinates.value.length; i++) {
content += `<trkpt lat="${clickCoordinates.value[i][1]}" lon="${clickCoordinates.value[i][0]}"><ele>0</ele><time>${date}</time></trkpt>`;
}
return `<?xml version="1.0" encoding="UTF-8" standalone="no" ?><gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" creator="cycle-rider.ru.ru" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd"><metadata><name>cycle-rider.ru</name><time>${date}</time></metadata><trk><name>cycle-rider.ru</name><trkseg>${content}</trkseg></trk></gpx>`;
};
const EARTH_RADIUS_KM = 6371; // Средний радиус Земли в километрах
function calculateTotalDistance(): number {
if (clickCoordinates.value.length < 2) return 0;
let totalDistance = 0;
for (let i = 1; i < clickCoordinates.value.length; i++) {
const [lon1, lat1] = clickCoordinates.value[i - 1];
const [lon2, lat2] = clickCoordinates.value[i];
totalDistance += calculateHaversineDistance(lon1, lat1, lon2, lat2);
}
return Math.round(totalDistance * 10) / 10;
}
function toRadians(degrees: number): number {
return (degrees * Math.PI) / 180;
}
function calculateHaversineDistance(
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): number {
const φ1 = toRadians(lat1);
const φ2 = toRadians(lat2);
const Δφ = toRadians(lat2 - lat1);
const Δλ = toRadians(lon2 - lon1);
const a =
Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_KM * c;
}
const downloadAsGPX = () => {
const gpx = toGPX();
const blob = new Blob([gpx], { type: "text/xml" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = "df";
link.click();
};
</script>
<style>
.map:deep([class$="main-engine-container"] canvas) {
cursor: pointer;
}
.info {
padding: 15px;
width: 300px;
font-size: 14px;
}
</style>