Optimalizace planovace, ikony pro letiste

This commit is contained in:
Michal Kváček
2024-05-27 22:31:06 +02:00
parent 97d1787642
commit 32ff47a3a7
8 changed files with 184 additions and 86 deletions
+4 -6
View File
@@ -1,5 +1,6 @@
<script setup>
import {computed} from "vue";
import {useAltitudeUtils} from "src/composables/airspaces";
const props = defineProps({
lowerLimit: {
@@ -20,13 +21,10 @@ const props = defineProps({
}
})
const getLimitFormatted = (limit, unit) => {
if (parseInt(limit) === 0) return 'GND';
return `${limit} ${unit}`
}
const { altitudeLimitFormatted } = useAltitudeUtils();
const upper = computed(() => getLimitFormatted(props.upperLimit, props.upperLimitUnit));
const lower = computed(() => getLimitFormatted(props.lowerLimit, props.lowerLimitUnit));
const upper = computed(() => altitudeLimitFormatted(props.upperLimit, props.upperLimitUnit));
const lower = computed(() => altitudeLimitFormatted(props.lowerLimit, props.lowerLimitUnit));
</script>
<template>
+7 -19
View File
@@ -40,10 +40,6 @@ const handleMarkerClick = (marker) => {
mapWrap.value.openInfoWindow({marker});
}
const handlePoiClick = (poi) => {
mapWrap.value.openInfoWindow({poi});
}
const markers = ref([]);
watch(() => props.flightPlan.track, (newVal) => {
markers.value = getMarkersFrom(props.flightPlan);
@@ -56,14 +52,17 @@ const addMarker = async (point) => {
if (!point) return;
const insertAt = Math.min(markers.value.length, props.insertPosition);
markers.value.splice(insertAt, 0, markerObject({...point}));
emits("markers-changed", markers.value);
emits("change-insert-position", insertAt + 1);
}
const removeMarker = (idx) => {
const removeMarker = (infoWindow) => {
const idx = markers.value.findIndex(i => i.gpsLatitude === infoWindow.position.lat && i.gpsLongitude === infoWindow.position.lng);
if (!idx) return;
markers.value.splice(idx, 1);
emits("markers-changed", markers.value);
}
@@ -85,6 +84,8 @@ const handleMarkerMove = async (idx, event) => {
style="width: 100%; height: 100%; min-height: 500px"
:center="center"
:zoom="8"
:track="{path: track, geodesic: true, strokeColor: '#ff00ff', strokeOpacity: 1, strokeWeight: 3}"
:points-of-interest="pointsOfInterest"
:airspaces-data="airspaceData"
:airports="airports"
v-bind="$attrs"
@@ -94,19 +95,7 @@ const handleMarkerMove = async (idx, event) => {
:allow-add-marker="true"
@add-to-route="addMarker"
@remove-from-route="removeMarker"
>
<template v-for="poi in pointsOfInterest" :key="poi.name">
<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.name">
<Marker
v-if="editableNameTypes.includes(marker.type)"
@@ -115,6 +104,5 @@ const handleMarkerMove = async (idx, event) => {
@dragend="handleMarkerMove(i, $event.latLng)"
/>
</template>
<Polyline :options="{path: track, geodesic: true, strokeColor: '#ff00ff', strokeOpacity: 1, strokeWeight: 3}"/>
</MapWrapper>
</template>
@@ -43,7 +43,6 @@ const totalTime = computed(() => {
<template>
<q-list bordered class="q-ml-md-md q-pt-md">
<q-item>
<q-item-section>
<div style="display: flex; justify-content: space-between">
@@ -66,8 +65,6 @@ const totalTime = computed(() => {
</q-item-section>
</q-item>
{{ insertPosition }} {{ flightPlan.track.length }}
<q-scroll-area style="height: 50vh">
<template v-for="(segment, i) in props.flightPlan.track" :key="segment">
<q-item dense>
+16 -14
View File
@@ -16,6 +16,11 @@ const props = defineProps({
required: false,
default: () => []
},
isInTrack: {
type: Boolean,
required: false,
default: false,
},
showActions: {
type: Boolean,
required: false,
@@ -34,7 +39,7 @@ 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);
searchPlaceName(props.infoWindow.position.lat, props.infoWindow.position.lng).then(data => title.value = data);
}
}, {immediate: true, deep: true});
@@ -55,8 +60,8 @@ const addPoint = () => {
emits("close");
}
const removePoint = (marker) => {
emits("remove-from-route", marker);
const removePoint = () => {
emits("remove-from-route", props.infoWindow);
emits("close");
}
@@ -132,17 +137,15 @@ const openedTab = ref("info");
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</div>
<div v-if="infoWindow.marker">
<span class="text-h6">
<q-icon :name="getIcon(infoWindow.marker.type)"/>
{{ infoWindow.marker.name }}
</span>
<span class="text-h6">
<q-icon :name="getIcon(infoWindow.marker.type)"/>
{{ infoWindow.marker.name }}
</span>
</div>
<div v-if="infoWindow.poi">
@@ -183,16 +186,15 @@ const openedTab = ref("info");
<template v-if="showActions">
<q-btn
v-if="infoWindow.marker"
class="q-mt-lg full-width"
v-if="isInTrack"
class="q-mr-sm q-mt-lg"
color="warning"
@click="removePoint(infoWindow.marker.position)"
@click="removePoint()"
>
Odebrat z trasy
</q-btn>
<q-btn
v-else
class="full-width q-mt-lg"
:class="{'q-mt-lg': true, 'full-width': !isInTrack}"
color="secondary"
@click="addPoint()"
>
+136 -42
View File
@@ -1,10 +1,11 @@
<script setup>
import {GoogleMap, CustomControl, CustomMarker, Polygon} from "vue3-google-map";
import {CustomControl, CustomMarker, GoogleMap, Polygon, Polyline} from "vue3-google-map";
import {computed, ref, watch} from "vue";
import {useVectorUtilities} from "src/graphql/fragments/vectorUtilities";
import {useAirspaceColors} from "src/composables/airspaces";
import {useAirspaceColors, useAltitudeUtils} from "src/composables/airspaces";
import AirspaceInfoWindow from "components/map/AirspaceInfoWindow.vue";
import {useFlightPlanObjects} from "src/composables/flightPlan";
import {debounce} from "quasar";
const props = defineProps({
bounds: {
@@ -21,6 +22,15 @@ const props = defineProps({
required: false,
default: () => []
},
pointsOfInterest: {
type: Array,
required: false,
default: () => []
},
track: {
type: Object,
required: false,
},
showInfoWindow: {
type: Boolean,
required: false,
@@ -42,8 +52,9 @@ const emits = defineEmits(["map-click", "add-to-route", "remove-from-route"]);
const key = process.env.GOOGLE_MAPS_API_KEY;
const zoomLevel = ref(null);
const mapRef = ref(null);
const zoomLevel = ref(null);
const visibleBoundsObject = ref(null);
watch(
() => mapRef.value?.ready,
(ready) => {
@@ -58,36 +69,55 @@ watch(
}
zoomLevel.value = mapRef.value.map.getZoom();
mapRef.value.map.addListener("zoom_changed", () => {
console.log("zoom changed");
mapRef.value.map.addListener("zoom_changed", debounce(() => {
zoomLevel.value = mapRef.value.map.getZoom();
})
}, 100));
mapRef.value.map.addListener("bounds_changed", debounce(() => {
visibleBoundsObject.value = mapRef.value.map.getBounds();
}, 100));
}
);
const airspaceLayers = ref({
CTR: {displayed: true, color: "", minZoom: 7},
TMA: {displayed: true, color: "", minZoom: 7},
RMZ: {displayed: false, color: "", minZoom: 7},
DANGER: {label: "Nebezpečné prostory", displayed: false, color: "", minZoom: 7},
PROHIBIT: {label: "Zakázané prostory", displayed: true, color: "", minZoom: 7},
RESTRICT: {label: "TRA", displayed: true, color: "", minZoom: 7},
TRAGA: {displayed: true, color: "", minZoom: 8},
ATZ: {displayed: true, color: "", minZoom: 8},
AIRPORTS: {label: "Letiště", displayed: true, color: "", minZoom: 10},
FIELDSTRIP: {label: "SLZ plochy", displayed: false, color: "", minZoom: 11},
})
const displayAirspaceLayers = ref({
CTR: true,
TMA: true,
RMZ: false,
ATZ: true,
PARA: false,
DANGER: false,
PROHIBIT: true,
RESTRICT: true,
TRAGA: true,
AIRPORTS: true,
FIELDSTRIP: false,
});
const isAirspaceVisible = (airspace, bounds) => {
if (!bounds) return false;
if (airspace.mapBounds) {
return airspace.mapBounds.some(b => bounds.contains(b));
}
if (airspace.gpsLatitude && airspace.gpsLongitude) {
return bounds.contains({lat: airspace.gpsLatitude, lng: airspace.gpsLongitude});
}
return true;
}
const maxAltitude = ref(5000);
const {altitudeLimitFormatted, altitudeToFt} = useAltitudeUtils();
const airspaces = computed(() => {
const colorsMap = useAirspaceColors().colors;
return props.airspacesData
.filter(airspace => !!colorsMap[airspace.type])
.filter(airspace => displayAirspaceLayers.value[airspace.type])
.filter(airspace => airspaceLayers.value[airspace.type] && airspaceLayers.value[airspace.type].displayed)
.filter(airspace => airspaceLayers.value[airspace.type].minZoom <= zoomLevel.value)
.filter(airspace => isAirspaceVisible(airspace, visibleBoundsObject.value))
.filter(airspace => altitudeToFt(airspace.lowerLimit, airspace.lowerAltUnit) < maxAltitude.value)
.map(airspace => {
let fillOpacity = 0;
let strokeWeight = 1;
if (["PROHIBIT", "RESTRICT"].includes(airspace.type))
@@ -115,9 +145,15 @@ const airspaces = computed(() => {
});
const airportsOnMap = computed(() => {
if (!displayAirspaceLayers.value.AIRPORTS && !displayAirspaceLayers.value.FIELDSTRIP) return [];
const displayAirport = airspaceLayers.value.AIRPORTS.displayed;
const displayFieldstrip = airspaceLayers.value.FIELDSTRIP.displayed;
if (!displayAirport && !displayFieldstrip) return [];
return props.airports.filter(a => a.gpsLatitude && a.gpsLongitude).filter(a => a.icaoCode.length === 6 ? displayAirspaceLayers.value.FIELDSTRIP : displayAirspaceLayers.value.AIRPORTS);
return props.airports
.filter(a => a.gpsLatitude && a.gpsLongitude)
.filter(a => isAirspaceVisible(a, visibleBoundsObject.value))
.filter(a => zoomLevel.value >= (a.icaoCode.length === 6 ? airspaceLayers.value.FIELDSTRIP.minZoom : airspaceLayers.value.AIRPORTS.minZoom))
.filter(a => a.icaoCode.length === 6 ? displayFieldstrip : displayAirport)
});
const {isPointInPolygon} = useVectorUtilities();
@@ -154,7 +190,7 @@ const highlightAirspace = (airspaceName) => {
if (airspace) {
highlightedAirspaces.value = [airspaceName];
displayAirspaceLayers.value[airspace.type] = true;
airspaceLayers.value[airspace.type].displayed = true;
}
}
@@ -184,6 +220,21 @@ const handleAirportClick = (airport) => {
emitMapClick({position, airport, airspaces: airspacesUnderClick})
}
const handlePoiClick = (poi) => {
const position = {lat: poi.gpsLatitude, lng: poi.gpsLongitude};
const airspacesUnderClick = getAirspacesAtCoordinates(position);
highlightedAirspaces.value = [];
emitMapClick({poi, position, airspaces: airspacesUnderClick});
}
const isDetailInTrack = computed(() => {
return !!props.track.path.find(i => i.lat === infoWindow.value.position.lat && i.lng === infoWindow.value.position.lng);
});
const isControlsOpened = ref(false);
</script>
<template>
@@ -193,6 +244,7 @@ const handleAirportClick = (airport) => {
:street-view-control="false"
@click="handleMapClick"
map-type-id="roadmap"
gesture-handling="greedy"
:max-zoom="13"
ref="mapRef"
>
@@ -201,6 +253,7 @@ const handleAirportClick = (airport) => {
:info-window="infoWindow"
:show-actions="allowAddMarker"
:highlighted-airspaces="highlightedAirspaces"
:is-in-track="isDetailInTrack"
@close="infoWindow.active=false"
@add-to-route="emits('add-to-route', $event)"
@remove-from-route="emits('remove-from-route', $event)"
@@ -218,37 +271,78 @@ const handleAirportClick = (airport) => {
:options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}"
@click.stop="handleAirportClick(airport)"
>
<q-chip size="xs">{{ airport.icaoCode }}</q-chip>
<img
:title="airport.icaoCode"
:src="airport.iconUrl"
:alt="airport.icaoCode"
/>
</CustomMarker>
<template v-if="displayAirspaceLayers.CTR">
<template v-if="airspaceLayers.CTR.displayed">
<CustomMarker
v-for="reportingPoint in airport.reportingPoints" :key="reportingPoint.id"
:options="{position: {lat: reportingPoint.gpsLatitude, lng: reportingPoint.gpsLongitude, anchorPoint: 'CENTER'}}"
@click.stop="handleReportingPointClick(airport, reportingPoint)"
:title="`${airport.icaoCode}: ${reportingPoint.name}`"
style="font-size: 20px"
>
<q-icon name="mdi-triangle"/>
&#9650;
</CustomMarker>
</template>
<template v-for="poi in pointsOfInterest" :key="poi.name">
<CustomMarker
v-if="poi.gpsLatitude && poi.gpsLongitude"
:options="{position: {lat: poi.gpsLatitude, lng: poi.gpsLongitude}, anchorPoint: 'BOTTOM_CENTER'}"
@click.stop="handlePoiClick(poi)"
>
<div style="padding: 5px; border-radius: 15px; background: #8bdfff">
<img style="width: 20px" src="/public/mapIcons/photo-32x32.png" alt="">
</div>
</CustomMarker>
</template>
<Polyline v-if="track" :options="track"/>
</template>
<CustomControl position="BOTTOM_CENTER" class="text-center" v-if="showLayerControls">
<q-card class="q-my-md q-mx-auto q-px-lg" style="max-width: 98%;">
<q-checkbox v-model="displayAirspaceLayers.CTR" label="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="TRA"/>
<q-checkbox v-model="displayAirspaceLayers.TRAGA" label="TRAGA"/>
<q-checkbox v-model="displayAirspaceLayers.PARA" label="PARA"/>
<q-checkbox v-model="displayAirspaceLayers.AIRPORTS" label="Letiště"/>
<q-checkbox v-model="displayAirspaceLayers.FIELDSTRIP" label="SLZ plochy"/>
<CustomControl position="BOTTOM_LEFT" v-if="showLayerControls" style="width: 30%">
<q-btn
@click="isControlsOpened = !isControlsOpened"
:icon="isControlsOpened ? 'layers_clear' : 'layers'"
color="grey-2"
text-color="grey-9"
round
size="sm"
:class="{'q-mb-lg': !isControlsOpened}"
>
</q-btn>
<q-card
v-if="isControlsOpened"
class="q-my-md q-mx-auto q-px-lg"
>
<template v-for="(airspaceLayer, key) in airspaceLayers" :key="airspaceLayer">
<q-checkbox
v-model="airspaceLayers[key].displayed"
v-if="airspaceLayers[key].minZoom <= zoomLevel"
:label="airspaceLayer.label || key"
/>
</template>
<br>
<q-slider
class="q-my-md"
v-model.number="maxAltitude"
:min="1000"
:max="15000"
label
label-always
:label-value="altitudeLimitFormatted(maxAltitude, 'ft')"
:step="1000"
/>
</q-card>
</CustomControl>
</GoogleMap>
</template>
+18
View File
@@ -18,3 +18,21 @@ export const useAirspaceColors = () => {
return {colors, getColorByType}
}
export const useAltitudeUtils = () => {
const altitudeToFt = (limit, unit) => {
if (unit === 'FL') return parseInt(limit) * 100;
return parseInt(limit);
}
const altitudeLimitFormatted = (limit, unit) => {
if (parseInt(limit) === 0) return 'GND';
else if (unit === 'FL') return `FL${limit}`;
else {
if (parseInt(limit) >= 5000) return `FL${limit / 100}`;
return `${limit} ft`
}
}
return {altitudeLimitFormatted, altitudeToFt}
}
+2 -2
View File
@@ -52,7 +52,7 @@ export const useFlightPlanObjects = () => {
}
}
const infoWindowObject = ({active, position, marker, poi, airport, reportingPoint, airspaces}) => {
const infoWindowObject = ({active, position, marker, poi, airport, reportingPoint, airspaces, isInTrack}) => {
if (!position) position = {};
if (!position.lat) position.lat = 0;
if (!position.lng) position.lng = 0;
@@ -83,7 +83,7 @@ export const useFlightPlanObjects = () => {
position.lng = poi.gpsLongitude;
}
return {title, active, position, airspaces, poi, marker, airport, reportingPoint};
return {title, active, position, airspaces, poi, marker, airport, reportingPoint, isInTrack};
};
return {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, infoWindowObject, editableNameTypes}
+1
View File
@@ -8,6 +8,7 @@ export const FRAGMENT_AIRPORT_INFO = gql`
gpsLatitude
gpsLongitude
elevation
iconUrl
reportingPoints {
id
gpsLatitude