Dalsi iterace planovace, snad jiz kompletni vrstvy mapy

This commit is contained in:
Michal Kváček
2024-04-17 13:21:32 +02:00
parent cee979fec8
commit de0b75163d
10 changed files with 745 additions and 430 deletions
@@ -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>
+94 -5
View File
@@ -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>
+76
View File
@@ -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>
+98 -52
View File
@@ -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>