Optimalizace planovace, ikony pro letiste
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,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()"
|
||||
>
|
||||
|
||||
@@ -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"/>
|
||||
▲
|
||||
</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,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}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const FRAGMENT_AIRPORT_INFO = gql`
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
elevation
|
||||
iconUrl
|
||||
reportingPoints {
|
||||
id
|
||||
gpsLatitude
|
||||
|
||||
Reference in New Issue
Block a user