Dalsi iterace planovace, snad jiz kompletni vrstvy mapy
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {Marker, CustomMarker, Polyline} from "vue3-google-map";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import AirspaceInfoWindow from "components/map/AirspaceInfoWindow.vue";
|
||||
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
airspaceData: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
airports: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pointsOfInterest: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const emits = defineEmits(["markers-changed"]);
|
||||
|
||||
const {center} = useGeocode();
|
||||
|
||||
const {markerObject, googleMapsCustomMarkerObject, googleMapsMarkerObject, infoWindowObject, editableNameTypes} = useFlightPlanObjects();
|
||||
const {getIcon, getMarkersFrom} = useFlightPlanUtils();
|
||||
|
||||
const infoWindow = ref(infoWindowObject({}));
|
||||
|
||||
const handleMapClick = ({clickCoordinates, airspaces, airport}) => {
|
||||
console.log(clickCoordinates, airspaces, airport);
|
||||
infoWindow.value = infoWindowObject({active: true, airspaces, position: clickCoordinates, airport})
|
||||
}
|
||||
|
||||
const handleMarkerClick = (marker) => {
|
||||
infoWindow.value = infoWindowObject({active: true, marker})
|
||||
}
|
||||
|
||||
const handlePoiClick = (poi) => {
|
||||
infoWindow.value = infoWindowObject({active: true, poi})
|
||||
}
|
||||
|
||||
const markers = ref([]);
|
||||
|
||||
watch(() => props.flightPlan.track, (newVal) => {
|
||||
markers.value = getMarkersFrom(props.flightPlan);
|
||||
}, {immediate: true, deep: true});
|
||||
|
||||
const track = computed(() => markers.value.map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||
|
||||
const addMarker = async (point) => {
|
||||
if (!point) return;
|
||||
markers.value.push(markerObject({...point}));
|
||||
|
||||
emits("markers-changed", markers.value);
|
||||
infoWindow.value = infoWindowObject({active: false});
|
||||
}
|
||||
|
||||
const {searchPlaceName} = useGeocode();
|
||||
const handleMarkerMove = async (idx, event) => {
|
||||
markers.value[idx].name = await searchPlaceName(event.lat(), event.lng());
|
||||
markers.value[idx].gpsLatitude = event.lat();
|
||||
markers.value[idx].gpsLongitude = event.lng();
|
||||
markers.value[idx].type = "mapPoint"
|
||||
|
||||
emits("markers-changed", markers.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MapWrapper
|
||||
style="width: 100%; height: 100%; min-height: 500px"
|
||||
:center="center"
|
||||
:zoom="8"
|
||||
:airspaces-data="airspaceData"
|
||||
:airports="airports"
|
||||
@map-click="handleMapClick"
|
||||
>
|
||||
<AirspaceInfoWindow
|
||||
v-if="infoWindow.active"
|
||||
:info-window="infoWindow"
|
||||
@close="infoWindow.active=false"
|
||||
@add-to-route="addMarker"
|
||||
/>
|
||||
|
||||
<template v-for="poi in pointsOfInterest" :key="poi">
|
||||
<CustomMarker
|
||||
v-if="poi.gpsLatitude && poi.gpsLongitude"
|
||||
:options="{position: {lat: poi.gpsLatitude, lng: poi.gpsLongitude}, anchorPoint: 'BOTTOM_CENTER'}"
|
||||
@click.stop="handlePoiClick(poi)"
|
||||
>
|
||||
<q-icon name="place" style="color: orange; font-size: 30px"/>
|
||||
</CustomMarker>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-for="(marker, i) in markers" :key="marker">
|
||||
<Marker
|
||||
v-if="editableNameTypes.includes(marker.type)"
|
||||
:options="googleMapsMarkerObject(marker)"
|
||||
@click="handleMarkerClick(marker)"
|
||||
@dragend="handleMarkerMove(i, $event.latLng)"
|
||||
/>
|
||||
<!-- <CustomMarker-->
|
||||
<!-- v-else-if="['poi', 'airport']marker.type !== 'poi'"-->
|
||||
<!-- :options="googleMapsCustomMarkerObject(marker)"-->
|
||||
<!-- @click="handleMarkerClick(marker)"-->
|
||||
<!-- >-->
|
||||
<!-- <q-icon :name="getIcon(marker.type)" style="font-size: 30px;color: #810000"/>-->
|
||||
<!-- </CustomMarker>-->
|
||||
</template>
|
||||
<Polyline :options="{path: track, geodesic: true, strokeColor: '#ff0202', strokeOpacity: 0.5, strokeWeight: 5}"/>
|
||||
</MapWrapper>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
plannedSpeed: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 150
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["open"])
|
||||
|
||||
const {getIcon} = useFlightPlanUtils();
|
||||
|
||||
const getDuration = (distance, speed) => Math.round(distance / speed * 60)
|
||||
|
||||
const totalDistance = computed(() => {
|
||||
if (!props.flightPlan.track.length) return null;
|
||||
|
||||
return Math.round(props.flightPlan.track.map(t => t.distance).reduce((a, b) => a + b));
|
||||
});
|
||||
const totalTime = computed(() => {
|
||||
if (!props.flightPlan.track.length) return null;
|
||||
|
||||
return props.flightPlan.track.map(t => getDuration(t.distance, props.plannedSpeed)).reduce((a, b) => a + b)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list bordered class="q-ml-md-md q-pt-md">
|
||||
<template v-for="(segment, i) in props.flightPlan.track" :key="segment">
|
||||
<q-item dense>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
<q-icon :name="getIcon(segment.from.type)"/>
|
||||
{{ segment.from.name }}
|
||||
</q-item-label>
|
||||
<q-item-section class="q-py-lg text-grey-8" v-if="segment.distance">
|
||||
{{ segment.bearing }}°,
|
||||
{{ segment.distance }} km,
|
||||
{{ minutesToHumanReadable(getDuration(segment.distance, props.plannedSpeed)) }}
|
||||
</q-item-section>
|
||||
<q-item-label class="text-h6" v-if="segment.to && i === props.flightPlan.track.length - 1">
|
||||
<q-icon :name="getIcon(segment.to.type)"/>
|
||||
{{ segment.to.name }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<template v-if="totalDistance">
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-separator/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
Celková vzdálenost: {{ totalDistance }} km
|
||||
<br>
|
||||
Plánovaná doba letu: {{ minutesToHumanReadable(totalTime) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-btn @click="emits('open')" color="secondary">
|
||||
{{ totalDistance ? 'Upravit trasu' : 'Sestavit trasu' }}
|
||||
</q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(["select"])
|
||||
|
||||
const place = ref("");
|
||||
const placesOptions = ref(null);
|
||||
|
||||
const {searchGeocode} = useGeocode()
|
||||
|
||||
watch(() => place.value, (newVal) => {
|
||||
if (newVal)
|
||||
emits("select", newVal);
|
||||
place.value = null;
|
||||
});
|
||||
const placesFilter = (val, update, abort) => {
|
||||
update(() => {
|
||||
if (val && val !== '') {
|
||||
placesOptions.value = props.options.filter(p => p.name.includes(val));
|
||||
if (placesOptions.value.length === 0) {
|
||||
searchGeocode(val).then(results => {
|
||||
if (!results) return;
|
||||
|
||||
placesOptions.value = results.map(result => ({
|
||||
name: result.formatted_address,
|
||||
type: "geocode",
|
||||
id: null,
|
||||
gpsLatitude: result.geometry.location.lat,
|
||||
gpsLongitude: result.geometry.location.lng,
|
||||
}))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
placesOptions.value = props.options;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-select
|
||||
v-bind="commonProps"
|
||||
v-model="place"
|
||||
label="Místo"
|
||||
use-input
|
||||
input-debounce="100"
|
||||
map-options
|
||||
emit-value
|
||||
option-label="name"
|
||||
option-disable="disabled"
|
||||
:options="placesOptions"
|
||||
@filter="placesFilter"
|
||||
>
|
||||
<template #no-option>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
Toto místo tu nemám. Zkus psát dál, najdem něco v Google Places.
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
@@ -1,23 +1,112 @@
|
||||
<script setup>
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import FlightPlanPlaceInput from "components/flightPlans/FlightPlanPlaceInput.vue";
|
||||
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
|
||||
const props = defineProps({
|
||||
markers: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
pointsOfInterest: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
airports: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["markers-changed"]);
|
||||
|
||||
const model = ref(props.markers);
|
||||
const removeMarker = (idx) => {
|
||||
model.value.splice(idx, 1);
|
||||
}
|
||||
const swapMarkers = (idx1, idx2) => {
|
||||
const tmp = markers.value[idx1];
|
||||
markers.value[idx1] = markers.value[idx2];
|
||||
markers.value[idx2] = tmp;
|
||||
const tmp = model.value[idx1];
|
||||
model.value[idx1] = model.value[idx2];
|
||||
model.value[idx2] = tmp;
|
||||
};
|
||||
const handlePlaceSelect = (modelVal) => {
|
||||
console.log(modelVal);
|
||||
model.value.push(modelVal);
|
||||
}
|
||||
|
||||
watch(() => model.value, () => {
|
||||
emits("markers-changed", model.value);
|
||||
}, {deep: true});
|
||||
|
||||
const {getIcon} = useFlightPlanUtils();
|
||||
const {editableNameTypes} = useFlightPlanObjects();
|
||||
|
||||
const allPlacesOptions = computed(() => {
|
||||
return [
|
||||
...props.pointsOfInterest.map(poi => ({...poi, type: "poi"})),
|
||||
...props.airports.map(airport => ({
|
||||
...airport,
|
||||
name: `${airport.icaoCode} - ${airport.name}`,
|
||||
type: "airport",
|
||||
}))
|
||||
].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude}));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-scroll-area style="height: 60vh;">
|
||||
<q-list bordered separator class="full-width">
|
||||
<q-item v-for="(marker, i) in model" :key="marker">
|
||||
<q-item-section side>
|
||||
<q-icon :name="getIcon(marker.type)"></q-icon>
|
||||
</q-item-section>
|
||||
|
||||
sem prijde ten modal, ve kterem se edituje cela trasa
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="i > 0"
|
||||
text-color="primary"
|
||||
size="16px"
|
||||
flat dense round icon="arrow_upward"
|
||||
@click="swapMarkers(i, i-1)"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="i < model.length - 1"
|
||||
text-color="primary"
|
||||
flat dense round icon="arrow_downward"
|
||||
size="16px"
|
||||
@click="swapMarkers(i, i+1)"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<div class="cursor-pointer" v-if="editableNameTypes.includes(marker.type)">
|
||||
{{ marker.name }}
|
||||
<q-icon name="edit"/>
|
||||
<q-popup-edit v-model="marker.name" v-slot="scope">
|
||||
<q-input v-model="scope.value" autofocus @keyup.enter="scope.set">
|
||||
<template #append>
|
||||
<q-icon name="edit"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{ marker.name }}
|
||||
</template>
|
||||
</q-item-section>
|
||||
<q-item-section side class="text-right">
|
||||
<q-btn size="12px" @click="removeMarker(i)" flat dense round color="red" icon="delete"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
|
||||
<FlightPlanPlaceInput
|
||||
:options="allPlacesOptions"
|
||||
@select="handlePlaceSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import {InfoWindow} from "vue3-google-map";
|
||||
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
infoWindow: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["add-to-route", "close"])
|
||||
|
||||
const {getIcon} = useFlightPlanUtils();
|
||||
const {searchPlaceName} = useGeocode()
|
||||
|
||||
const title = ref(null);
|
||||
|
||||
watch(() => props.infoWindow.position, async () => {
|
||||
if (props.infoWindow.title) {
|
||||
title.value = props.infoWindow.title
|
||||
} else {
|
||||
title.value = await searchPlaceName(props.infoWindow.position.lat, props.infoWindow.position.lng);
|
||||
}
|
||||
}, {immediate: true, deep: true});
|
||||
|
||||
const addPoint = async () => {
|
||||
let type = "mapPoint";
|
||||
if (props.infoWindow.poi) {
|
||||
type = "poi";
|
||||
} else if (props.infoWindow.airport) {
|
||||
type = "airport";
|
||||
}
|
||||
const point = props.infoWindow.marker || {
|
||||
name: title.value,
|
||||
type: type,
|
||||
gpsLatitude: props.infoWindow.position.lat,
|
||||
gpsLongitude: props.infoWindow.position.lng
|
||||
}
|
||||
emits("add-to-route", point)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<InfoWindow
|
||||
@close="emits('close')"
|
||||
:options="{position: infoWindow.position}"
|
||||
>
|
||||
<div class="text-h5">{{ title }}</div>
|
||||
|
||||
<div v-if="infoWindow.marker">
|
||||
<span class="text-h6">
|
||||
<q-icon :name="getIcon(infoWindow.marker.type)"/>
|
||||
{{ infoWindow.marker.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="infoWindow.poi">
|
||||
<q-img :src="infoWindow.poi.titlePhoto.thumbnailUrl" v-if="infoWindow.poi.titlePhoto"/>
|
||||
{{ infoWindow.poi.name }}
|
||||
|
||||
{{ infoWindow.poi }}
|
||||
</div>
|
||||
|
||||
<template v-if="infoWindow.airspaces?.length">
|
||||
<span class="text-h6">Prostory</span>
|
||||
<div v-for="airspace in infoWindow.airspaces" :key="airspace">
|
||||
{{ airspace.name }}
|
||||
</div>
|
||||
</template>
|
||||
<q-btn class="q-mt-lg" color="secondary" @click="addPoint()">Přidat bod do trasy</q-btn>
|
||||
</InfoWindow>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import {Circle, GoogleMap, CustomMarker, Polygon} from "vue3-google-map";
|
||||
import {Circle, GoogleMap, CustomControl, CustomMarker, Polygon} from "vue3-google-map";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useBasicVectorOperations, useVectorUtities} from "src/graphql/fragments/vectorUtilities";
|
||||
import {useBasicVectorOperations, useVectorUtilities} from "src/graphql/fragments/vectorUtilities";
|
||||
|
||||
const props = defineProps({
|
||||
bounds: {
|
||||
@@ -13,8 +13,16 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
airports: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["map-click"]);
|
||||
|
||||
|
||||
const key = process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
const mapRef = ref(null);
|
||||
@@ -34,6 +42,19 @@ watch(
|
||||
);
|
||||
|
||||
|
||||
const displayAirspaceLayers = ref({
|
||||
CTR: true,
|
||||
TMA: true,
|
||||
RMZ: true,
|
||||
ATZ: true,
|
||||
DANGER: true,
|
||||
PROHIBIT: true,
|
||||
RESTRICT: true,
|
||||
TRA: true,
|
||||
TRAGA: true,
|
||||
AIRPORTS: true
|
||||
});
|
||||
|
||||
const airspaces = computed(() => {
|
||||
const colorsMap = {
|
||||
C: "#800080",
|
||||
@@ -44,19 +65,30 @@ const airspaces = computed(() => {
|
||||
R: "#ff6a00",
|
||||
Q: "#FFA500",
|
||||
PGZ: "#008000",
|
||||
GSEC: "#008000"
|
||||
GSEC: "#008000",
|
||||
ATZ: "#4e7cfc"
|
||||
}
|
||||
|
||||
return props.airspacesData
|
||||
.filter(airspace => !!colorsMap[airspace.type])
|
||||
.filter(airspace => {
|
||||
if (['C', 'D'].includes(airspace.type)) return displayAirspaceLayers.value.TMA;
|
||||
if (['PGZ', 'GSEC'].includes(airspace.type)) return displayAirspaceLayers.value.TRAGA;
|
||||
if (airspace.type === 'CTR') return displayAirspaceLayers.value.CTR;
|
||||
if (airspace.type === 'RMZ') return displayAirspaceLayers.value.RMZ;
|
||||
if (airspace.type === 'Q') return displayAirspaceLayers.value.DANGER;
|
||||
if (airspace.type === 'P') return displayAirspaceLayers.value.PROHIBIT;
|
||||
if (airspace.type === 'R') return displayAirspaceLayers.value.RESTRICT;
|
||||
return true;
|
||||
})
|
||||
.map(airspace => {
|
||||
const data = {
|
||||
name: airspace.name,
|
||||
strokeColor: colorsMap[airspace.type],
|
||||
strokeOpacity: 0.2,
|
||||
strokeOpacity: 0.3,
|
||||
strokeWeight: 3,
|
||||
fillColor: colorsMap[airspace.type],
|
||||
fillOpacity: 0.1,
|
||||
fillOpacity: 0,
|
||||
};
|
||||
|
||||
if (airspace.mapBounds.length > 0) {
|
||||
@@ -70,74 +102,88 @@ const airspaces = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const atzs = computed(() => {
|
||||
return props.airspacesData
|
||||
.filter(a => a.type === 'ATZ')
|
||||
.filter(a => !!a.mapCenter)
|
||||
.map(airspace => ({
|
||||
name: airspace.name,
|
||||
strokeColor: "#4e7cfc",
|
||||
strokeOpacity: 0.2,
|
||||
strokeWeight: 1,
|
||||
fillColor: "#4e7cfc",
|
||||
fillOpacity: 0.1,
|
||||
center: airspace.mapCenter,
|
||||
radius: airspace.radius,
|
||||
}))
|
||||
const airportsOnMap = computed(() => {
|
||||
if (!displayAirspaceLayers.value.AIRPORTS) return [];
|
||||
|
||||
return props.airports.filter(a => a.gpsLatitude && a.gpsLongitude);
|
||||
});
|
||||
|
||||
// TODO: pridat letiste
|
||||
|
||||
const {isPointInPolygon} = useVectorUtities();
|
||||
const {isPointInPolygon, vectorDistanceToKm} = useVectorUtilities();
|
||||
const {vectorDistance} = useBasicVectorOperations();
|
||||
|
||||
const handleLayerClick = (e) => {
|
||||
const clickCoordinates = {lat: e.latLng.lat(), lng: e.latLng.lng()}
|
||||
|
||||
const getAirspacesAtCoordinates = (clickCoordinates) => {
|
||||
const airspacesUnderClick = []
|
||||
|
||||
for (const atz of atzs.value) {
|
||||
const distance = vectorDistance(
|
||||
{gpsLatitude: e.latLng.lat(), gpsLongitude: e.latLng.lng()},
|
||||
{gpsLatitude: atz.center.lat, gpsLongitude: atz.center.lng},
|
||||
);
|
||||
|
||||
if (distance < 0.079) { // tohle je nejaka bulharska konstanta, +- 5km od stredu letiste
|
||||
airspacesUnderClick.push(atz);
|
||||
}
|
||||
}
|
||||
|
||||
for (const airspace of airspaces.value) {
|
||||
if (airspace.path && isPointInPolygon(clickCoordinates, airspace.path)) {
|
||||
for (const airspace of props.airspacesData) {
|
||||
if (airspace.mapBounds && isPointInPolygon(clickCoordinates, airspace.mapBounds)) {
|
||||
airspacesUnderClick.push(airspace);
|
||||
} else if (airspace.mapCenter && airspace.radius) {
|
||||
const radDistance = vectorDistance(clickCoordinates, airspace.mapCenter);
|
||||
const distanceKm = vectorDistanceToKm(radDistance)
|
||||
|
||||
if (distanceKm * 1000 <= airspace.radius) {
|
||||
airspacesUnderClick.push(airspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
emits("map-click", {airspaces: airspacesUnderClick, clickCoordinates});
|
||||
return airspacesUnderClick;
|
||||
}
|
||||
|
||||
const emits = defineEmits(["map-click"]);
|
||||
|
||||
const handleMapClick = (e) => {
|
||||
const clickCoordinates = {lat: e.latLng.lat(), lng: e.latLng.lng()}
|
||||
emits("map-click", {clickCoordinates});
|
||||
};
|
||||
emits("map-click", {airspaces: getAirspacesAtCoordinates(clickCoordinates), clickCoordinates});
|
||||
}
|
||||
|
||||
const handleAirportClick = (airport) => {
|
||||
const position = {lat: airport.gpsLatitude, lng: airport.gpsLongitude};
|
||||
emits("map-click", {clickCoordinates: position, airport})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GoogleMap :api-key="key" v-bind="$attrs" @click="handleMapClick" map-type-id="terrain" :max-zoom="13" ref="mapRef">
|
||||
<slot/>
|
||||
<GoogleMap
|
||||
:api-key="key"
|
||||
v-bind="$attrs"
|
||||
:street-view-control="false"
|
||||
@click="handleMapClick"
|
||||
map-type-id="terrain"
|
||||
:max-zoom="13"
|
||||
ref="mapRef"
|
||||
>
|
||||
<CustomControl position="BOTTOM_CENTER" class="text-center">
|
||||
<q-card class="q-my-lg q-px-lg">
|
||||
<q-checkbox v-model="displayAirspaceLayers.CTR" label="M/CTR"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.TMA" label="TMA"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.RMZ" label="RMZ"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.ATZ" label="ATZ"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.DANGER" label="Nebezpečné prostory"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.PROHIBIT" label="Zakázané prostory"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.RESTRICT" label="Omezené prostory (TRA)"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.TRAGA" label="TRAGA"/>
|
||||
<q-checkbox v-model="displayAirspaceLayers.AIRPORTS" label="Letiště"/>
|
||||
</q-card>
|
||||
</CustomControl>
|
||||
|
||||
<template v-for="airspace in airspaces" :key="airspace">
|
||||
<Polygon :options="airspace" @click=handleLayerClick v-if="airspace.path"/>
|
||||
<Circle :options="airspace" @click=handleLayerClick v-if="airspace.center && airspace.radius"/>
|
||||
<Polygon :options="airspace" @click=handleMapClick v-if="airspace.path"/>
|
||||
<Circle :options="airspace" @click=handleMapClick v-if="airspace.center && airspace.radius"/>
|
||||
</template>
|
||||
|
||||
<template v-for="atz in atzs" :key="atz">
|
||||
<Circle :options="atz" @click=handleLayerClick />
|
||||
<CustomMarker :options="{position: atz.center, anchorPoint: 'CENTER'}" @click="handleLayerClick">
|
||||
<q-icon name="local_airport" style="font-size: 24px" class="text-primary"/>
|
||||
<template v-for="airport in airportsOnMap" :key="airport.icaoCode">
|
||||
<CustomMarker
|
||||
:options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}"
|
||||
@click.stop="handleAirportClick(airport)"
|
||||
>
|
||||
<div class="text-center">
|
||||
<q-icon name="local_airport" style="font-size: 24px" class="text-primary"/>
|
||||
<br>
|
||||
{{ airport.icaoCode }}
|
||||
</div>
|
||||
</CustomMarker>
|
||||
</template>
|
||||
|
||||
<slot/>
|
||||
|
||||
</GoogleMap>
|
||||
</template>
|
||||
|
||||
@@ -9,5 +9,76 @@ export const useFlightPlanUtils = () => {
|
||||
return icons[type] || "place"
|
||||
}
|
||||
|
||||
return { getIcon }
|
||||
const getMarkersFrom = (flightPlan) => {
|
||||
const segments = flightPlan.track.length;
|
||||
|
||||
const markers = flightPlan.track.map(m => m.from);
|
||||
if (segments > 0 && flightPlan.track[segments - 1].to) {
|
||||
markers.push(flightPlan.track[segments - 1].to);
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
return {getIcon, getMarkersFrom}
|
||||
}
|
||||
|
||||
export const useFlightPlanObjects = () => {
|
||||
const editableNameTypes = ["geocode", "mapPoint"];
|
||||
|
||||
const markerObject = ({name, type, gpsLatitude, gpsLongitude}) => ({
|
||||
name, type, gpsLatitude, gpsLongitude
|
||||
});
|
||||
|
||||
const googleMapsCustomMarkerObject = (marker) => {
|
||||
const anchorPoints = {
|
||||
airport: "CENTER",
|
||||
geocode: "BOTTOM_CENTER",
|
||||
poi: "BOTTOM_CENTER",
|
||||
mapPoint: "BOTTOM_CENTER",
|
||||
}
|
||||
|
||||
return {
|
||||
anchorPoint: anchorPoints[marker.type],
|
||||
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||
}
|
||||
}
|
||||
|
||||
const googleMapsMarkerObject = (marker) => {
|
||||
return {
|
||||
label: marker.name,
|
||||
draggable: editableNameTypes.includes(marker.type),
|
||||
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||
}
|
||||
}
|
||||
|
||||
const infoWindowObject = ({active, position, marker, poi, airport, airspaces}) => {
|
||||
if (!position) position = {};
|
||||
if (!position.lat) position.lat = 0;
|
||||
if (!position.lng) position.lng = 0;
|
||||
|
||||
let title = null;
|
||||
|
||||
if (airport) {
|
||||
title = `${airport.icaoCode} (${airport.name})`;
|
||||
position.lat = airport.gpsLatitude;
|
||||
position.lng = airport.gpsLongitude;
|
||||
}
|
||||
|
||||
if (marker) {
|
||||
title = marker.name;
|
||||
position.lat = marker.gpsLatitude;
|
||||
position.lng = marker.gpsLongitude;
|
||||
}
|
||||
|
||||
if (poi) {
|
||||
title = poi.name;
|
||||
position.lat = poi.gpsLatitude;
|
||||
position.lng = poi.gpsLongitude;
|
||||
}
|
||||
|
||||
return {title, active, position, airspaces, poi, marker, airport};
|
||||
};
|
||||
|
||||
return {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, infoWindowObject, editableNameTypes}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import {ref} from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
export const useGeocode = (emits) => {
|
||||
export const useGeocode = () => {
|
||||
|
||||
const center = ref({
|
||||
lat: 50.1314,
|
||||
lng: 14.5256
|
||||
});
|
||||
const zoom = ref(10);
|
||||
const center = ref({lat: 50.1314, lng: 14.5256});
|
||||
|
||||
const emitModels = (lat, lng) => {
|
||||
emits("update:gpsLatitude", lat);
|
||||
emits("update:gpsLongitude", lng);
|
||||
}
|
||||
|
||||
const searchGeocode = (address) => {
|
||||
return axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURI(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
||||
@@ -24,18 +16,29 @@ export const useGeocode = (emits) => {
|
||||
});
|
||||
}
|
||||
|
||||
const setCoordinatesFromGeocode = (address) => {
|
||||
// TODO: odstranit a nahradit za searchGeocode! Vlastne cela tato composable by sla nahradit
|
||||
axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURI(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
||||
if (response.data.status !== 'OK') {
|
||||
emits("notFound", address);
|
||||
const searchPlaceName = (lat, lng) => {
|
||||
return axios.get(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
||||
if (response.data.status !== 'OK' || !response.data.results?.length) {
|
||||
return null;
|
||||
}
|
||||
center.value = response.data.results[0].geometry.location;
|
||||
emitModels(center.value.lat, center.value.lng);
|
||||
zoom.value = 13;
|
||||
|
||||
const results = response.data.results;
|
||||
const components = {};
|
||||
for (const addressComponent of results[0].address_components) {
|
||||
for (const namePartType of ["locality", "postal_town", 'administrative_area_level_2']) {
|
||||
if (addressComponent.types.includes(namePartType)) {
|
||||
components[namePartType] = addressComponent.long_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(components).length) {
|
||||
return "Bod na mapě";
|
||||
}
|
||||
|
||||
return Object.values(components).join(", ");
|
||||
});
|
||||
}
|
||||
|
||||
return {emitModels, setCoordinatesFromGeocode, zoom, center, searchGeocode}
|
||||
return {center, searchGeocode, searchPlaceName}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ export const useBasicVectorOperations = () => {
|
||||
}
|
||||
|
||||
const vectorDistance = (pointA, pointB) => {
|
||||
console.log("pointA", pointA, "pointB", pointB);
|
||||
|
||||
const u = [pointA.gpsLatitude, pointA.gpsLongitude];
|
||||
const v = [pointB.gpsLatitude, pointB.gpsLongitude];
|
||||
const u = [pointA.gpsLatitude || pointA.lat, pointA.gpsLongitude || pointA.lng];
|
||||
const v = [pointB.gpsLatitude || pointB.lat, pointB.gpsLongitude || pointB.lng];
|
||||
// const v = vector(pointB);
|
||||
|
||||
return Math.sqrt(
|
||||
@@ -22,9 +20,7 @@ export const useBasicVectorOperations = () => {
|
||||
return {vector, vectorSize, vectorDistance}
|
||||
}
|
||||
|
||||
export const useVectorUtities = () => {
|
||||
|
||||
|
||||
export const useVectorUtilities = () => {
|
||||
function isPointInPolygon(clickPoint, polygonCoords) {
|
||||
let count = 0;
|
||||
const x = clickPoint.lng, y = clickPoint.lat;
|
||||
@@ -42,5 +38,9 @@ export const useVectorUtities = () => {
|
||||
return count % 2 === 1; // Lichý počet průsečíků = bod uvnitř
|
||||
}
|
||||
|
||||
return {isPointInPolygon}
|
||||
const vectorDistanceToKm = (vectorDistance) => {
|
||||
return vectorDistance * (2*Math.PI * 6371 / 360);
|
||||
}
|
||||
|
||||
return {isPointInPolygon, vectorDistanceToKm}
|
||||
}
|
||||
|
||||
@@ -9,175 +9,20 @@ export default {
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {Marker, CustomMarker, Polyline, InfoWindow} from "vue3-google-map";
|
||||
import {useAirspacesStore} from "stores/Airspaces";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
import {useAirportsStore} from "stores/Airports";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {useAppStore} from "stores/App";
|
||||
|
||||
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
|
||||
import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue";
|
||||
import FlightPlanMarkersList from "components/flightPlans/FlightPlanMarkersList.vue";
|
||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
|
||||
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||
const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||
const {airspaces: airspacesData, areAirspacesLoading: isQueryRunning} = storeToRefs(useAirspacesStore())
|
||||
const {airports} = storeToRefs(useAirportsStore());
|
||||
|
||||
onMounted(() => {
|
||||
useAirspacesStore().fetch();
|
||||
usePointsOfInterestStore().fetch({limit: 10000});
|
||||
useAirportsStore().fetch();
|
||||
});
|
||||
|
||||
|
||||
const emits = defineEmits(["update:gpsLatitude", "update:gpsLongitude", "notFound"]);
|
||||
const {center, zoom, searchGeocode} = useGeocode(emits);
|
||||
zoom.value = 8;
|
||||
|
||||
|
||||
const placesOptions = ref(null);
|
||||
const allPlacesOptions = computed(() => {
|
||||
return [
|
||||
...pointsOfInterest.value.map(poi => ({...poi, type: "poi"})),
|
||||
...airports.value.map(airport => ({
|
||||
...airport,
|
||||
name: `${airport.icaoCode} - ${airport.name}`,
|
||||
type: "airport",
|
||||
}))
|
||||
].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude}));
|
||||
});
|
||||
|
||||
const placesFilter = (val, update, abort) => {
|
||||
update(() => {
|
||||
if (val && val !== '') {
|
||||
placesOptions.value = allPlacesOptions.value.filter(p => p.name.includes(val));
|
||||
if (placesOptions.value.length === 0) {
|
||||
searchGeocode(val).then(results => {
|
||||
if (!results) return;
|
||||
|
||||
placesOptions.value = results.map(result => ({
|
||||
name: result.formatted_address,
|
||||
type: "geocode",
|
||||
id: null,
|
||||
gpsLatitude: result.geometry.location.lat,
|
||||
gpsLongitude: result.geometry.location.lng,
|
||||
}))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
placesOptions.value = allPlacesOptions.value;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const formData = ref({
|
||||
flightPlanName: flightPlan.value?.name || "",
|
||||
plannedSpeed: 150
|
||||
})
|
||||
|
||||
const place = ref("");
|
||||
const prompt = ref(false);
|
||||
const markers = ref([]);
|
||||
markers.value = flightPlan.value.track.map(m => m.from);
|
||||
if (flightPlan.value.track.length > 1) {
|
||||
markers.value.push(flightPlan.value.track[flightPlan.value.track.length - 1].to);
|
||||
}
|
||||
const track = computed(() => markers.value.map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||
|
||||
watch(() => place.value, (newVal) => {
|
||||
addPoint(newVal);
|
||||
place.value = null;
|
||||
});
|
||||
|
||||
|
||||
const markerObject = ({name, type, gpsLatitude, gpsLongitude}) => ({
|
||||
name, type, gpsLatitude, gpsLongitude
|
||||
});
|
||||
|
||||
const addPoint = async (point) => {
|
||||
if (!point) return;
|
||||
markers.value.push(markerObject({...point}));
|
||||
}
|
||||
const removeMarker = (idx) => {
|
||||
markers.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
|
||||
watch(() => markers.value, () => {
|
||||
useFlightPlansStore().update(flightPlan.value.id, {
|
||||
name: formData.value.flightPlanName,
|
||||
isPublic: false,
|
||||
markers: markers.value.map(m => markerObject({
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
gpsLatitude: m.gpsLatitude,
|
||||
gpsLongitude: m.gpsLongitude,
|
||||
}))
|
||||
});
|
||||
}, {deep: true});
|
||||
|
||||
const getIcon = (type) => {
|
||||
const icons = {
|
||||
"poi": "pin_drop",
|
||||
"geocode": "travel_explore",
|
||||
"airport": "local_airport"
|
||||
};
|
||||
|
||||
return icons[type] || "place"
|
||||
}
|
||||
|
||||
const swapMarkers = (idx1, idx2) => {
|
||||
const tmp = markers.value[idx1];
|
||||
markers.value[idx1] = markers.value[idx2];
|
||||
markers.value[idx2] = tmp;
|
||||
};
|
||||
|
||||
const getDuration = (distance, speed) => Math.round(distance / speed * 60)
|
||||
|
||||
const totalDistance = computed(() => {
|
||||
if (!flightPlan.value.track.length) return null;
|
||||
|
||||
return Math.round(flightPlan.value.track.map(t => t.distance).reduce((a, b) => a + b));
|
||||
});
|
||||
const totalTime = computed(() => {
|
||||
if (!flightPlan.value.track.length) return null;
|
||||
|
||||
return flightPlan.value.track.map(t => getDuration(t.distance, formData.value.plannedSpeed)).reduce((a, b) => a + b)
|
||||
});
|
||||
|
||||
const infoWindowObject = ({active, position, marker, airspaces}) => {
|
||||
if (!position) position = {};
|
||||
if (!position.lat) position.lat = 0;
|
||||
if (!position.lng) position.lng = 0;
|
||||
|
||||
if (marker) {
|
||||
position.lat = marker.gpsLatitude;
|
||||
position.lng = marker.gpsLongitude;
|
||||
}
|
||||
|
||||
return {active, position, airspaces, marker};
|
||||
};
|
||||
const infoWindow = ref(infoWindowObject({}));
|
||||
|
||||
const handleMapClick = ({clickCoordinates, airspaces}) => {
|
||||
infoWindow.value = infoWindowObject({active: true, airspaces, position: clickCoordinates})
|
||||
}
|
||||
|
||||
const handleMarkerClick = (marker) => {
|
||||
infoWindow.value = infoWindowObject({active: true, marker})
|
||||
}
|
||||
|
||||
const handleMarkerMove = (idx, event) => {
|
||||
markers.value[idx].gpsLatitude = event.lat();
|
||||
markers.value[idx].gpsLongitude = event.lng();
|
||||
}
|
||||
|
||||
const editableNameTypes = ["geocode", "mapPoint"];
|
||||
|
||||
const flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán");
|
||||
useAppStore().setPageTitle(`${flightPlanName.value} - plánovnání`);
|
||||
@@ -187,211 +32,125 @@ useAppStore().setBreadcrumbs([
|
||||
{label: "Editace"}
|
||||
]);
|
||||
|
||||
const googleMapsMarkerObject = (marker) => {
|
||||
const anchorPoints = {
|
||||
airport: "CENTER",
|
||||
geocode: "BOTTOM_CENTER",
|
||||
poi: "BOTTOM_CENTER",
|
||||
mapPoint: "BOTTOM_CENTER",
|
||||
}
|
||||
onMounted(() => {
|
||||
useAirspacesStore().fetch();
|
||||
usePointsOfInterestStore().fetch({limit: 10000});
|
||||
useAirportsStore().fetch();
|
||||
});
|
||||
|
||||
return {
|
||||
// label: marker.name,
|
||||
anchorPoint: anchorPoints[marker.type],
|
||||
draggable: editableNameTypes.includes(marker.type),
|
||||
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||
}
|
||||
const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||
const {airspaces: airspacesData, areAirspacesLoading: isQueryRunning} = storeToRefs(useAirspacesStore())
|
||||
const {airports} = storeToRefs(useAirportsStore());
|
||||
|
||||
const {getMarkersFrom} = useFlightPlanUtils();
|
||||
const {markerObject} = useFlightPlanObjects();
|
||||
|
||||
const formData = ref({
|
||||
name: flightPlan.value?.name || null,
|
||||
isPublic: flightPlan.value?.isPublic || false,
|
||||
takeoffTime: flightPlan.value?.takeoffTime || null,
|
||||
aircraft: flightPlan.value?.aircraft || null,
|
||||
plannedSpeed: flightPlan.value?.plannedSpeed || 150
|
||||
})
|
||||
|
||||
const openedTab = ref("route");
|
||||
const flightPlannerDialogOpen = ref(false);
|
||||
const flightPlanMarkers = ref(getMarkersFrom(flightPlan.value));
|
||||
|
||||
const updateFlightPlanName = () => {
|
||||
formData.value.name = flightPlanMarkers.value.map(m => m.name).join(" - ");
|
||||
}
|
||||
|
||||
|
||||
const handleMarkersChanges = (markers) => {
|
||||
flightPlanMarkers.value = markers;
|
||||
|
||||
useFlightPlansStore().update(flightPlan.value.id, {
|
||||
name: formData.value.name,
|
||||
markers: markers.map(m => markerObject({
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
gpsLatitude: m.gpsLatitude,
|
||||
gpsLongitude: m.gpsLongitude,
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const handleFlightPlanInfoSave = () => useFlightPlansStore().update(flightPlan.value.id, formData.value);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input v-model="formData.flightPlanName" label="Jméno letu" v-bind="commonProps"/>
|
||||
</div>
|
||||
</div>
|
||||
<q-tabs v-model="openedTab">
|
||||
<q-tab name="route">
|
||||
<q-icon name="route"/>
|
||||
Trasa
|
||||
</q-tab>
|
||||
|
||||
<div class="row">
|
||||
<div class="col" style="min-width: 1px">
|
||||
<MapWrapper
|
||||
style="width: 100%; height: 100%; min-height: 500px"
|
||||
:center="center"
|
||||
:zoom="zoom"
|
||||
:airspaces-data="airspacesData"
|
||||
@map-click="handleMapClick"
|
||||
>
|
||||
<q-tab name="info">
|
||||
<q-icon name="info"/>
|
||||
Informace
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
|
||||
<InfoWindow v-if="infoWindow.active" @close="infoWindow.active=false" :options="{position: infoWindow.position, title: 'titulek'}">
|
||||
|
||||
<div class="text-h5">{{ infoWindow.marker?.name || "Bod v mapě" }}</div>
|
||||
|
||||
<div v-if="infoWindow.marker">
|
||||
<span class="text-h6">
|
||||
<q-icon :name="getIcon(infoWindow.marker.type)"/>
|
||||
{{ infoWindow.marker.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="infoWindow.airspaces?.length">
|
||||
<span class="text-h6">Prostory</span>
|
||||
<div v-for="airspace in infoWindow.airspaces" :key="airspace">
|
||||
{{ airspace.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<q-btn class="q-mt-lg" color="secondary" @click="addPoint(infoWindow.marker || {name: `Bod ${markers.length}`, type: 'mapPoint', gpsLatitude: infoWindow.position.lat, gpsLongitude: infoWindow.position.lng} )">Přidat bod do trasy</q-btn>
|
||||
</InfoWindow>
|
||||
|
||||
<template v-for="(marker, i) in markers" :key="marker">
|
||||
|
||||
<Marker
|
||||
v-if="editableNameTypes.includes(marker.type)"
|
||||
:options="googleMapsMarkerObject(marker)"
|
||||
@click="handleMarkerClick(marker)"
|
||||
@dragend="handleMarkerMove(i, $event.latLng)"
|
||||
<q-tab-panels v-model="openedTab">
|
||||
<q-tab-panel name="route">
|
||||
<div class="row">
|
||||
<div class="col" style="min-width: 1px">
|
||||
<FlightPlanMap
|
||||
:airspace-data="airspacesData"
|
||||
:airports="airports"
|
||||
:points-of-interest="pointsOfInterest"
|
||||
:flight-plan="flightPlan"
|
||||
@markers-changed="handleMarkersChanges"
|
||||
/>
|
||||
<CustomMarker
|
||||
v-else
|
||||
:options="googleMapsMarkerObject(marker)"
|
||||
@click="handleMarkerClick(marker)"
|
||||
>
|
||||
<q-icon :name="getIcon(marker.type)" style="font-size: 30px;color: #810000"/>
|
||||
</CustomMarker>
|
||||
</template>
|
||||
<Polyline :options="{path: track, geodesic: true, strokeColor: '#ff0202', strokeOpacity: 0.5, strokeWeight: 5}"/>
|
||||
</MapWrapper>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 col-lg-3">
|
||||
<q-list bordered class="q-ml-md-md q-pt-md">
|
||||
<!-- <q-item>-->
|
||||
<!-- <q-item-section>-->
|
||||
<!-- <q-separator/>-->
|
||||
<!-- </q-item-section>-->
|
||||
<!-- </q-item>-->
|
||||
</div>
|
||||
<div class="col-12 col-md-5 col-lg-3">
|
||||
<FlightPlanMarkersList
|
||||
:flight-plan="flightPlan"
|
||||
@open="flightPlannerDialogOpen=true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<template v-for="(segment, i) in flightPlan.track" :key="segment">
|
||||
<q-item dense>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-h6">
|
||||
<q-icon :name="getIcon(segment.from.type)"/>
|
||||
{{ segment.from.name }}
|
||||
</q-item-label>
|
||||
<q-item-section class="q-py-lg text-grey-8">
|
||||
{{ segment.bearing }}°,
|
||||
{{ segment.distance }} km,
|
||||
{{ minutesToHumanReadable(getDuration(segment.distance, formData.plannedSpeed)) }}
|
||||
</q-item-section>
|
||||
<q-item-label class="text-h6" v-if="i === flightPlan.track.length - 1">
|
||||
<q-icon :name="getIcon(segment.to.type)"/>
|
||||
{{ segment.to.name }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<q-tab-panel name="info">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-form @submit.prevent="handleFlightPlanInfoSave">
|
||||
<q-input v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||
<q-btn @click="updateFlightPlanName()">Jmeno automaticky</q-btn>
|
||||
|
||||
<template v-if="totalDistance">
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-separator/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
Celková vzdálenost: {{ totalDistance }} km
|
||||
<br>
|
||||
Plánovaná doba letu: {{ minutesToHumanReadable(totalTime) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-btn @click="prompt=true" color="secondary">
|
||||
<q-checkbox v-model="formData.isPublic" label="Zveřejnit"/>
|
||||
<DatetimePicker v-model="formData.takeoffTime" label="Datum a čas vzletu" v-bind="commonProps"/>
|
||||
<q-select v-model="formData.aircraft" :options="[]" label="Letadlo" v-bind="commonProps"/>
|
||||
|
||||
{{ totalDistance ? 'Upravit trasu' : 'Sestavit trasu' }}
|
||||
</q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input v-model="formData.plannedSpeed" label="Plánovaná rychlost" v-bind="commonProps">
|
||||
<template #append>km/h</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog full-width v-model="prompt">
|
||||
<q-btn @click="handleFlightPlanInfoSave" icon="save">Ulozit</q-btn>
|
||||
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
|
||||
<q-dialog full-width v-model="flightPlannerDialogOpen">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Přidat otočný bod</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-scroll-area style="height: 60vh;">
|
||||
<q-list bordered separator class="full-width">
|
||||
<q-item v-for="(marker, i) in markers" :key="marker">
|
||||
|
||||
<q-item-section side>
|
||||
<q-icon :name="getIcon(marker.type)"></q-icon>
|
||||
</q-item-section>
|
||||
|
||||
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-btn text-color="primary" v-if="i > 0" size="16px" @click="swapMarkers(i, i-1)" flat dense round icon="arrow_upward"/>
|
||||
<q-btn text-color="primary" v-if="i < markers.length - 1" size="16px" @click="swapMarkers(i, i+1)" flat dense round icon="arrow_downward"/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
|
||||
<q-item-section>
|
||||
<div class="cursor-pointer" v-if="editableNameTypes.includes(marker.type)">
|
||||
{{ marker.name }}
|
||||
<q-icon name="edit"/>
|
||||
<q-popup-edit v-model="marker.name" v-slot="scope">
|
||||
<q-input v-model="scope.value" autofocus @keyup.enter="scope.set">
|
||||
<template #append>
|
||||
<q-icon name="edit"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{ marker.name }}
|
||||
</template>
|
||||
</q-item-section>
|
||||
<q-item-section side class="text-right">
|
||||
<q-btn size="12px" @click="removeMarker(i)" flat dense round color="red" icon="delete"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
|
||||
<q-select
|
||||
v-bind="commonProps"
|
||||
v-model="place"
|
||||
label="Místo"
|
||||
use-input
|
||||
input-debounce="100"
|
||||
map-options
|
||||
emit-value
|
||||
option-label="name"
|
||||
option-disable="disabled"
|
||||
:options="placesOptions"
|
||||
@filter="placesFilter"
|
||||
>
|
||||
<template #no-option>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
Pis dal, najdeme neco na GoogleMaps!
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<FlightPlanRoute
|
||||
:markers="flightPlanMarkers"
|
||||
:points-of-interest="pointsOfInterest"
|
||||
:airports="airports"
|
||||
@markers-changed="handleMarkersChanges"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
|
||||
Reference in New Issue
Block a user