Zakladni vytvareni letu na zaklade planu

This commit is contained in:
Michal Kváček
2024-06-30 02:31:16 +02:00
parent 16650cc0cb
commit ba1089af3d
16 changed files with 240 additions and 23 deletions
+1 -1
View File
@@ -30,6 +30,6 @@ const saveForm = () => {
<template> <template>
<q-form @submit.prevent="saveForm" ref="form"> <q-form @submit.prevent="saveForm" ref="form">
<slot name="default"/> <slot name="default"/>
<FormSubmitButton v-if="saveButton" :save-button="saveButton"/> <FormSubmitButton v-if="saveButton" :save-button="saveButton" :is-request-pending="isRequestPending" />
</q-form> </q-form>
</template> </template>
+33 -3
View File
@@ -1,4 +1,6 @@
<script setup> <script setup>
import {computed} from "vue";
const props = defineProps({ const props = defineProps({
routeAdd: { routeAdd: {
type: Object, type: Object,
@@ -8,16 +10,44 @@ const props = defineProps({
type: String, type: String,
required: false, required: false,
default: "Přidat" default: "Přidat"
},
color: {
type: String,
required: false,
default: 'info'
} }
}); });
const bannerClass = computed(() => {
const defaultClasses = 'bg-light-blue text-white';
const classMap = {
info: defaultClasses,
warning: 'bg-orange text-white',
error: 'bg-red text-white',
}
return classMap[props.color] || defaultClasses;
});
const icon = computed(() => {
const iconsMap = {
info: "info",
warning: "warning",
error: "error",
}
return iconsMap[props.color];
});
</script> </script>
<template> <template>
<div class="q-pa-md q-gutter-sm"> <div class="q-pa-md q-gutter-sm">
<q-banner class="bg-light-blue text-white"> <q-banner :class=bannerClass>
<template v-slot:avatar> <template v-slot:avatar v-if="icon">
<q-icon name="info"/> <q-icon :name="icon"/>
</template> </template>
<slot/> <slot/>
<template v-slot:action v-if="routeAdd"> <template v-slot:action v-if="routeAdd">
@@ -0,0 +1,53 @@
<script setup>
import DatetimePicker from "components/inputs/DatetimePicker.vue";
import {computed, reactive} from "vue";
import {commonProps} from "src/composables/inputs";
import FormWrapper from "components/FormWrapper.vue";
import {useFlightPlanCalculations, useFlightPlanUtils} from "src/composables/flightPlan";
const props = defineProps({
flightPlan: {
type: Object,
required: true
}
});
const {getTotalDuration} = useFlightPlanCalculations();
const formData = reactive({
gpxTrackFile: null,
takeoffDatetime: props.flightPlan.plannedTakeoffDatetime,
landingDatetime: new Date((new Date(props.flightPlan.plannedTakeoffDatetime)).getTime() + 60 * 1000 * getTotalDuration(props.flightPlan)),
copyTrack: true,
createPointsOfInterests: true,
flightPlan: props.flightPlan,
});
const {getMarkersFrom} = useFlightPlanUtils();
const hasGeocodeInTrack = computed(() => {
return !!getMarkersFrom(props.flightPlan).find(point => point.type === 'geocode');
});
</script>
<template>
<FormWrapper v-model="formData" :save-button="{label: 'Vytvořit let z plánu', icon: 'save'}">
<template v-if="!formData.gpxTrackFile">
<DatetimePicker v-model="formData.takeoffDatetime" label="Čas vzletu"/>
<DatetimePicker v-model="formData.landingDatetime" label="Čas přistání"/>
</template>
<q-file
v-bind="commonProps"
v-model="formData.gpxTrackFile"
label="Trasa v GPX"
hint="Z trasy se načtou letiště a časy vzletu a přistání. Tyto hodnoty je možné později upravit."
/>
<q-checkbox
v-model="formData.copyTrack"
label="Zkopírovat otočné body (letiště a POI)?"
/>
</FormWrapper>
</template>
+33 -4
View File
@@ -32,7 +32,7 @@ const emits = defineEmits(["markers-changed", "change-insert-position"]);
const {center} = useGeocode(); const {center} = useGeocode();
const {markerObject, googleMapsMarkerObject, editableNameTypes} = useFlightPlanObjects(); const {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, editableNameTypes} = useFlightPlanObjects();
const {getMarkersFrom} = useFlightPlanUtils(); const {getMarkersFrom} = useFlightPlanUtils();
const {getDuration} = useFlightPlanCalculations(); const {getDuration} = useFlightPlanCalculations();
@@ -54,8 +54,8 @@ const trackAids = computed(() => props.flightPlan.track.map(p => ({
bearing: p.bearing, bearing: p.bearing,
distance: p.distance, distance: p.distance,
position: { position: {
lat: (p.from.gpsLatitude + p.to.gpsLatitude) / 2, lat: (p.from.gpsLatitude + p.to?.gpsLatitude) / 2,
lng: (p.from.gpsLongitude + p.to.gpsLongitude) / 2 lng: (p.from.gpsLongitude + p.to?.gpsLongitude) / 2
} }
} }
))); )));
@@ -92,6 +92,7 @@ const handleMarkerMove = async (idx, event) => {
</script> </script>
<template> <template>
<MapWrapper <MapWrapper
ref="mapWrap" ref="mapWrap"
style="width: 100%; height: 65vh; min-height: 500px" style="width: 100%; height: 65vh; min-height: 500px"
@@ -130,13 +131,41 @@ const handleMarkerMove = async (idx, event) => {
</template> </template>
</template> </template>
<template v-if="markers.length > 1">
<CustomMarker :options="googleMapsCustomMarkerObject(markers[0], 'CENTER')" class="significant-airport">
<q-icon name="flight_takeoff"/>
</CustomMarker>
<CustomMarker :options="googleMapsCustomMarkerObject(markers[markers.length - 1], 'CENTER')" class="significant-airport">
<q-icon name="flight_land"/>
</CustomMarker>
</template>
<template #layersControlsEnd> <template #layersControlsEnd>
<q-checkbox v-model="displayTrackAids" label="Zobrazovat navigaci po trase?"/> <q-checkbox v-model="displayTrackAids" label="Zobrazovat navigaci po trase?"/>
</template> </template>
</MapWrapper> </MapWrapper>
</template> </template>
<style> <style lang="scss">
.significant-airport {
position: relative;
z-index: 99999;
text-align: center;
margin-top: -40px;
.q-icon {
color: white;
background: #39b6e6;
border: solid 1px black;
width: 30px;
height: 30px;
font-size: 20px;
border-radius: 50%;
}
}
.track-info { .track-info {
background: rgba(128, 128, 128, 0.5); background: rgba(128, 128, 128, 0.5);
color: white; color: white;
@@ -30,7 +30,6 @@ const swapMarkers = (idx1, idx2) => {
model.value[idx2] = tmp; model.value[idx2] = tmp;
}; };
const handlePlaceSelect = (modelVal) => { const handlePlaceSelect = (modelVal) => {
console.log(modelVal);
model.value.push(modelVal); model.value.push(modelVal);
} }
@@ -42,10 +41,9 @@ const {getIcon} = useFlightPlanUtils();
const {editableNameTypes} = useFlightPlanObjects(); const {editableNameTypes} = useFlightPlanObjects();
const allPlacesOptions = computed(() => { const allPlacesOptions = computed(() => {
const reportingPoints = props.airports.map(a => a.reportingPoints.map(r => ({...r, airport: a}))).flat() const reportingPoints = props.airports.map(a => a.reportingPoints.map(r => ({...r, airport: a}))).flat()
return [ return [
...props.pointsOfInterest.map(poi => ({...poi, type: "poi"})), ...props.pointsOfInterest.map(poi => ({...poi, pointOfInterestId: poi.id, type: "poi"})),
...reportingPoints.map(r => ({ ...reportingPoints.map(r => ({
...r, ...r,
name: `${r.airport.icaoCode}:${r.name}`, name: `${r.airport.icaoCode}:${r.name}`,
@@ -55,6 +53,7 @@ const allPlacesOptions = computed(() => {
...airport, ...airport,
name: `${airport.icaoCode} - ${airport.name}`, name: `${airport.icaoCode} - ${airport.name}`,
type: "airport", type: "airport",
airportId: airport.id,
})) }))
].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude})); ].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude}));
}); });
@@ -104,7 +104,7 @@ const totalDistance = computed(() => getTotalDistance(props.flightPlan));
<q-item-section> <q-item-section>
Celková vzdálenost: {{ totalDistance }} km Celková vzdálenost: {{ totalDistance }} km
<br> <br>
Plánovaná doba letu: {{ minutesToHumanReadable(getTotalDuration(props.flightPlan, props.flightPlan.plannedSpeed)) }} Plánovaná doba letu: {{ minutesToHumanReadable(getTotalDuration(props.flightPlan)) }}
</q-item-section> </q-item-section>
</q-item> </q-item>
</template> </template>
@@ -56,6 +56,5 @@ watch(() => props.flight, (newVal) => {
:label="i === 1 ? 'Kopilot' : 'Pasažér'" :label="i === 1 ? 'Kopilot' : 'Pasažér'"
/> />
</div> </div>
</FormWrapper> </FormWrapper>
</template> </template>
@@ -30,6 +30,7 @@ const steps = [
{name: "track", icon: "route", label: "Trasa"}, {name: "track", icon: "route", label: "Trasa"},
{name: "photos", icon: "add_a_photo", label: "Fotky"}, {name: "photos", icon: "add_a_photo", label: "Fotky"},
{name: "notes", icon: "edit_note", label: "Poznámky"}, {name: "notes", icon: "edit_note", label: "Poznámky"},
{name: "plan", icon: "pin_drop", label: "Plán"},
]; ];
const emit = defineEmits(["update:modelValue"]) const emit = defineEmits(["update:modelValue"])
+4 -2
View File
@@ -50,9 +50,12 @@ const addPoint = () => {
} else if (props.infoWindow.airport) { } else if (props.infoWindow.airport) {
type = "airport"; type = "airport";
} }
const point = props.infoWindow.marker || { const point = props.infoWindow.marker || {
name: title.value, name: title.value,
type: type, airportId: props.infoWindow.airport?.id,
pointOfInterestId: props.infoWindow.poi?.id,
type,
gpsLatitude: props.infoWindow.position.lat, gpsLatitude: props.infoWindow.position.lat,
gpsLongitude: props.infoWindow.position.lng gpsLongitude: props.infoWindow.position.lng
} }
@@ -63,7 +66,6 @@ const addPoint = () => {
const closeInfoWindow = () => { const closeInfoWindow = () => {
emits('close'); emits('close');
isOpened.value = false; isOpened.value = false;
console.log("is opened? ", isOpened.value);
} }
const removePoint = () => { const removePoint = () => {
+3 -1
View File
@@ -180,6 +180,7 @@ defineExpose({
}); });
const emitMapClick = (data) => { const emitMapClick = (data) => {
isControlsOpened.value = false;
openInfoWindow(data); openInfoWindow(data);
emits("map-click", data); emits("map-click", data);
} }
@@ -271,6 +272,7 @@ const isControlsOpened = ref(false);
<template v-for="airport in airportsOnMap" :key="airport.icaoCode"> <template v-for="airport in airportsOnMap" :key="airport.icaoCode">
<CustomMarker <CustomMarker
style="position: relative; z-index: 100"
:options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}" :options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}"
@click.stop="handleAirportClick(airport)" @click.stop="handleAirportClick(airport)"
> >
@@ -316,7 +318,7 @@ const isControlsOpened = ref(false);
<Polyline v-if="track" :options="track"/> <Polyline v-if="track" :options="track"/>
<CustomControl position="TOP" v-if="showLayerControls" style="width: auto; max-width: 200px"> <CustomControl position="TOP_LEFT" v-if="showLayerControls" style="width: auto; max-width: 200px">
<q-btn <q-btn
@click="isControlsOpened = !isControlsOpened" @click="isControlsOpened = !isControlsOpened"
:icon="isControlsOpened ? 'layers_clear' : 'layers'" :icon="isControlsOpened ? 'layers_clear' : 'layers'"
+6 -5
View File
@@ -32,7 +32,8 @@ export const useFlightPlanCalculations = () => {
return Math.round(flightPlan.track.map(t => t.distance).reduce((a, b) => a + b)); return Math.round(flightPlan.track.map(t => t.distance).reduce((a, b) => a + b));
} }
const getTotalDuration = (flightPlan, plannedSpeed) => { const getTotalDuration = (flightPlan, plannedSpeed = undefined) => {
if (!plannedSpeed) plannedSpeed = flightPlan.plannedSpeed;
if (!flightPlan.track.length) return null; if (!flightPlan.track.length) return null;
return flightPlan.track.map(t => getDuration(t.distance, plannedSpeed)).reduce((a, b) => a + b) return flightPlan.track.map(t => getDuration(t.distance, plannedSpeed)).reduce((a, b) => a + b)
@@ -44,11 +45,11 @@ export const useFlightPlanCalculations = () => {
export const useFlightPlanObjects = () => { export const useFlightPlanObjects = () => {
const editableNameTypes = ["geocode", "mapPoint"]; const editableNameTypes = ["geocode", "mapPoint"];
const markerObject = ({name, type, gpsLatitude, gpsLongitude}) => ({ const markerObject = ({name, type, airportId, pointOfInterestId, gpsLatitude, gpsLongitude}) => ({
name, type, gpsLatitude, gpsLongitude name, type, gpsLatitude, gpsLongitude, pointOfInterestId, airportId
}); });
const googleMapsCustomMarkerObject = (marker) => { const googleMapsCustomMarkerObject = (marker, overrideAnchorPoint = undefined) => {
const anchorPoints = { const anchorPoints = {
airport: "CENTER", airport: "CENTER",
geocode: "BOTTOM_CENTER", geocode: "BOTTOM_CENTER",
@@ -57,7 +58,7 @@ export const useFlightPlanObjects = () => {
} }
return { return {
anchorPoint: anchorPoints[marker.type], anchorPoint: overrideAnchorPoint || anchorPoints[marker.type],
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude} position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
} }
} }
+1
View File
@@ -100,6 +100,7 @@ ${FRAGMENT_WEATHER_FULL_INFO}
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql` export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
fragment flightFullPrivateInfo on Flight { fragment flightFullPrivateInfo on Flight {
...flightFullInfo ...flightFullInfo
flightPlanId
debriefingNotes debriefingNotes
copilots { copilots {
+2
View File
@@ -7,6 +7,8 @@ fragment flightPlanMarker on FlightPlanMarker {
type type
gpsLatitude gpsLatitude
gpsLongitude gpsLongitude
pointOfInterestId
airportId
} }
` `
+16 -1
View File
@@ -36,6 +36,7 @@ import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
import {usePhotosStore} from "stores/Photos"; import {usePhotosStore} from "stores/Photos";
import {flightName} from "src/composables/flight"; import {flightName} from "src/composables/flight";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useFlightPlansStore} from "stores/FlightPlans";
const $q = useQuasar(); const $q = useQuasar();
const $route = useRoute(); const $route = useRoute();
@@ -47,9 +48,9 @@ const {airports} = storeToRefs(useAirportsStore());
const {copilots} = storeToRefs(useCopilotsStore()); const {copilots} = storeToRefs(useCopilotsStore());
const {events} = storeToRefs(useEventsStore()); const {events} = storeToRefs(useEventsStore());
const {photos} = storeToRefs(usePhotosStore()); const {photos} = storeToRefs(usePhotosStore());
const {flightPlan, flightPlans} = storeToRefs(useFlightPlansStore());
const {flight, isQueryRunning, isRequestPending: isMutationRunning} = storeToRefs(useFlightsStore()); const {flight, isQueryRunning, isRequestPending: isMutationRunning} = storeToRefs(useFlightsStore());
const formStep = ref("basic"); const formStep = ref("basic");
watch(() => $route.params.step, (val) => formStep.value = val); watch(() => $route.params.step, (val) => formStep.value = val);
@@ -71,6 +72,11 @@ onMounted(() => {
useAirportsStore().fetch(); useAirportsStore().fetch();
useCopilotsStore().fetch(); useCopilotsStore().fetch();
useEventsStore().fetch({}); useEventsStore().fetch({});
useFlightPlansStore().fetch();
if (flight.value.flightPlanId) {
useFlightPlansStore().fetchDetail(flight.value.flightPlanId);
}
}); });
const handleFlightEdit = (data) => { const handleFlightEdit = (data) => {
@@ -234,5 +240,14 @@ const handleBatchPhotoEdit = async (data) => {
<q-tab-panel name="notes"> <q-tab-panel name="notes">
<FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/> <FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="plan">
<template v-if="flight.flightPlanId">
{{ flightPlan.name }}
</template>
<template v-else>
Bez planu
</template>
</q-tab-panel>
</q-tab-panels> </q-tab-panels>
</template> </template>
@@ -20,10 +20,14 @@ import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue"; import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue";
import FlightPlanSideControls from "components/flightPlans/FlightPlanSideControls.vue"; import FlightPlanSideControls from "components/flightPlans/FlightPlanSideControls.vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {successObject} from "src/composables/dialog"; import {errorObject, successObject} from "src/composables/dialog";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import FlightPlanRouteTable from "components/flightPlans/FlightPlanRouteTable.vue"; import FlightPlanRouteTable from "components/flightPlans/FlightPlanRouteTable.vue";
import FlightPlanForm from "components/flightPlans/FlightPlanForm.vue"; import FlightPlanForm from "components/flightPlans/FlightPlanForm.vue";
import {useFlightsStore} from "stores/Flights";
import {useRouter} from "vue-router";
import FlightFromPlanForm from "components/flightPlans/FlightFromPlanForm.vue";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
const {flightPlan} = storeToRefs(useFlightPlansStore()); const {flightPlan} = storeToRefs(useFlightPlansStore());
const flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán"); const flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán");
@@ -60,6 +64,8 @@ const handleMarkersChanges = (markers) => {
markers: markers.map(m => markerObject({ markers: markers.map(m => markerObject({
name: m.name, name: m.name,
type: m.type, type: m.type,
airportId: m.airportId,
pointOfInterestId: m.pointOfInterestId,
gpsLatitude: m.gpsLatitude, gpsLatitude: m.gpsLatitude,
gpsLongitude: m.gpsLongitude, gpsLongitude: m.gpsLongitude,
})) }))
@@ -75,6 +81,67 @@ const handleFlightPlanInfoSave = (formData) => {
} }
const insertAtPosition = ref(flightPlanMarkers.value.length); const insertAtPosition = ref(flightPlanMarkers.value.length);
const fullscreen = ref(false); const fullscreen = ref(false);
const takeoffAirport = computed(() => {
const markers = getMarkersFrom(flightPlan.value)
return markers[0].type === 'airport' ? markers[0] : null;
})
const landingAirport = computed(() => {
const markers = getMarkersFrom(flightPlan.value);
const lastMarker = markers[markers.length - 1];
return lastMarker.type === 'airport' ? lastMarker : null;
})
const $router = useRouter();
const createNewFlightFromPlan = (formData) => {
const markers = getMarkersFrom(formData.flightPlan);
const takeoff = markers.shift();
const landing = markers.pop();
if ((takeoff.type !== 'airport' || landing.type !== 'airport') && !formData.gpxTrackFile) {
$q.notify(errorObject("Zkontroluj letiště vzletu a přistání. Aktuálně zvolené hodnoty se jako letiště nezdají. Pro pokračování uprav buď plán, nebo zkus nahrát trasu v GPX."));
return;
}
const track = [];
if (formData.copyTrack) {
// TODO: po ulozeni mapPoint/geocode??? upravit i trasu v planu - predelat ten mapPoint na POI s IDckem
for (const i in markers) {
const marker = markers[i];
if (i === 0 || i === markers.length - 1 || marker.type === 'mapPoint') continue;
const obj = {
[marker.type === 'airport' ? 'airport' : 'pointOfInterest']: {
id: marker.airportId || marker.pointOfInterestId || null,
name: marker.name,
},
gpsLatitude: marker.gpsLatitude,
gpsLongitude: marker.gpsLongitude
};
track.push(obj);
}
}
useFlightsStore().create({
name: formData.flightPlan.name,
aircraft: {id: formData.flightPlan.aircraft.id, name: formData.flightPlan.aircraft.callSign},
flightPlanId: formData.flightPlan.id,
takeoffAirport: takeoff ? {id: takeoff.airportId, name: takeoff.name} : null,
landingAirport: landing ? {id: landing.airportId, name: landing.name} : null,
gpxTrackFile: formData.gpxTrackFile,
takeoffDatetime: formData.takeoffDatetime,
landingDatetime: formData.landingDatetime,
track
}).then(res => {
$q.notify(successObject("Let úspěšně vytvořen. Nyní můžeš nastavit detaily."))
$router.push({name: 'flightEdit', params: {id: res.id, step: 'photo'}});
});
}
</script> </script>
<template> <template>
@@ -94,6 +161,11 @@ const fullscreen = ref(false);
Navigační štítek Navigační štítek
</q-tab> </q-tab>
<q-tab name="flight" v-if="flightPlan.aircraft">
<q-icon name="flight"/>
Let
</q-tab>
</q-tabs> </q-tabs>
<q-tab-panels v-model="openedTab"> <q-tab-panels v-model="openedTab">
<q-tab-panel name="route"> <q-tab-panel name="route">
@@ -141,6 +213,16 @@ const fullscreen = ref(false);
:planned-speed="flightPlan.plannedSpeed" :planned-speed="flightPlan.plannedSpeed"
/> />
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="flight" v-if="flightPlan.aircraft">
<BasicInfoBanner v-if="!takeoffAirport || !landingAirport" color="error">
V plánu nemáš žádné letiště vzletu nebo přistání. Pro import letu musíš nahrát trasu.
</BasicInfoBanner>
<FlightFromPlanForm
:flight-plan="flightPlan"
@save="createNewFlightFromPlan"
/>
</q-tab-panel>
</q-tab-panels> </q-tab-panels>
<q-dialog full-width v-model="flightPlannerDialogOpen"> <q-dialog full-width v-model="flightPlannerDialogOpen">
@@ -8,6 +8,7 @@ const $router = useRouter();
onMounted(async () => { onMounted(async () => {
await useFlightPlansStore().create({ await useFlightPlansStore().create({
name: "", name: "",
description: "",
isDefaultName: true, isDefaultName: true,
isPublic: false, isPublic: false,
isEditableByCopilot: false, isEditableByCopilot: false,