196 lines
6.1 KiB
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>
|