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>
<q-form @submit.prevent="saveForm" ref="form">
<slot name="default"/>
<FormSubmitButton v-if="saveButton" :save-button="saveButton"/>
<FormSubmitButton v-if="saveButton" :save-button="saveButton" :is-request-pending="isRequestPending" />
</q-form>
</template>
+33 -3
View File
@@ -1,4 +1,6 @@
<script setup>
import {computed} from "vue";
const props = defineProps({
routeAdd: {
type: Object,
@@ -8,16 +10,44 @@ const props = defineProps({
type: String,
required: false,
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>
<template>
<div class="q-pa-md q-gutter-sm">
<q-banner class="bg-light-blue text-white">
<template v-slot:avatar>
<q-icon name="info"/>
<q-banner :class=bannerClass>
<template v-slot:avatar v-if="icon">
<q-icon :name="icon"/>
</template>
<slot/>
<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 {markerObject, googleMapsMarkerObject, editableNameTypes} = useFlightPlanObjects();
const {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, editableNameTypes} = useFlightPlanObjects();
const {getMarkersFrom} = useFlightPlanUtils();
const {getDuration} = useFlightPlanCalculations();
@@ -54,8 +54,8 @@ const trackAids = computed(() => props.flightPlan.track.map(p => ({
bearing: p.bearing,
distance: p.distance,
position: {
lat: (p.from.gpsLatitude + p.to.gpsLatitude) / 2,
lng: (p.from.gpsLongitude + p.to.gpsLongitude) / 2
lat: (p.from.gpsLatitude + p.to?.gpsLatitude) / 2,
lng: (p.from.gpsLongitude + p.to?.gpsLongitude) / 2
}
}
)));
@@ -92,6 +92,7 @@ const handleMarkerMove = async (idx, event) => {
</script>
<template>
<MapWrapper
ref="mapWrap"
style="width: 100%; height: 65vh; min-height: 500px"
@@ -130,13 +131,41 @@ const handleMarkerMove = async (idx, event) => {
</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>
<q-checkbox v-model="displayTrackAids" label="Zobrazovat navigaci po trase?"/>
</template>
</MapWrapper>
</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 {
background: rgba(128, 128, 128, 0.5);
color: white;
@@ -30,7 +30,6 @@ const swapMarkers = (idx1, idx2) => {
model.value[idx2] = tmp;
};
const handlePlaceSelect = (modelVal) => {
console.log(modelVal);
model.value.push(modelVal);
}
@@ -42,10 +41,9 @@ const {getIcon} = useFlightPlanUtils();
const {editableNameTypes} = useFlightPlanObjects();
const allPlacesOptions = computed(() => {
const reportingPoints = props.airports.map(a => a.reportingPoints.map(r => ({...r, airport: a}))).flat()
return [
...props.pointsOfInterest.map(poi => ({...poi, type: "poi"})),
...props.pointsOfInterest.map(poi => ({...poi, pointOfInterestId: poi.id, type: "poi"})),
...reportingPoints.map(r => ({
...r,
name: `${r.airport.icaoCode}:${r.name}`,
@@ -55,6 +53,7 @@ const allPlacesOptions = computed(() => {
...airport,
name: `${airport.icaoCode} - ${airport.name}`,
type: "airport",
airportId: airport.id,
}))
].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude}));
});
@@ -104,7 +104,7 @@ const totalDistance = computed(() => getTotalDistance(props.flightPlan));
<q-item-section>
Celková vzdálenost: {{ totalDistance }} km
<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>
</template>
@@ -56,6 +56,5 @@ watch(() => props.flight, (newVal) => {
:label="i === 1 ? 'Kopilot' : 'Pasažér'"
/>
</div>
</FormWrapper>
</template>
@@ -30,6 +30,7 @@ const steps = [
{name: "track", icon: "route", label: "Trasa"},
{name: "photos", icon: "add_a_photo", label: "Fotky"},
{name: "notes", icon: "edit_note", label: "Poznámky"},
{name: "plan", icon: "pin_drop", label: "Plán"},
];
const emit = defineEmits(["update:modelValue"])
+4 -2
View File
@@ -50,9 +50,12 @@ const addPoint = () => {
} else if (props.infoWindow.airport) {
type = "airport";
}
const point = props.infoWindow.marker || {
name: title.value,
type: type,
airportId: props.infoWindow.airport?.id,
pointOfInterestId: props.infoWindow.poi?.id,
type,
gpsLatitude: props.infoWindow.position.lat,
gpsLongitude: props.infoWindow.position.lng
}
@@ -63,7 +66,6 @@ const addPoint = () => {
const closeInfoWindow = () => {
emits('close');
isOpened.value = false;
console.log("is opened? ", isOpened.value);
}
const removePoint = () => {
+3 -1
View File
@@ -180,6 +180,7 @@ defineExpose({
});
const emitMapClick = (data) => {
isControlsOpened.value = false;
openInfoWindow(data);
emits("map-click", data);
}
@@ -271,6 +272,7 @@ const isControlsOpened = ref(false);
<template v-for="airport in airportsOnMap" :key="airport.icaoCode">
<CustomMarker
style="position: relative; z-index: 100"
:options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}"
@click.stop="handleAirportClick(airport)"
>
@@ -316,7 +318,7 @@ const isControlsOpened = ref(false);
<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
@click="isControlsOpened = !isControlsOpened"
: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));
}
const getTotalDuration = (flightPlan, plannedSpeed) => {
const getTotalDuration = (flightPlan, plannedSpeed = undefined) => {
if (!plannedSpeed) plannedSpeed = flightPlan.plannedSpeed;
if (!flightPlan.track.length) return null;
return flightPlan.track.map(t => getDuration(t.distance, plannedSpeed)).reduce((a, b) => a + b)
@@ -44,11 +45,11 @@ export const useFlightPlanCalculations = () => {
export const useFlightPlanObjects = () => {
const editableNameTypes = ["geocode", "mapPoint"];
const markerObject = ({name, type, gpsLatitude, gpsLongitude}) => ({
name, type, gpsLatitude, gpsLongitude
const markerObject = ({name, type, airportId, pointOfInterestId, gpsLatitude, gpsLongitude}) => ({
name, type, gpsLatitude, gpsLongitude, pointOfInterestId, airportId
});
const googleMapsCustomMarkerObject = (marker) => {
const googleMapsCustomMarkerObject = (marker, overrideAnchorPoint = undefined) => {
const anchorPoints = {
airport: "CENTER",
geocode: "BOTTOM_CENTER",
@@ -57,7 +58,7 @@ export const useFlightPlanObjects = () => {
}
return {
anchorPoint: anchorPoints[marker.type],
anchorPoint: overrideAnchorPoint || anchorPoints[marker.type],
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`
fragment flightFullPrivateInfo on Flight {
...flightFullInfo
flightPlanId
debriefingNotes
copilots {
+2
View File
@@ -7,6 +7,8 @@ fragment flightPlanMarker on FlightPlanMarker {
type
gpsLatitude
gpsLongitude
pointOfInterestId
airportId
}
`
+16 -1
View File
@@ -36,6 +36,7 @@ import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
import {usePhotosStore} from "stores/Photos";
import {flightName} from "src/composables/flight";
import {useAppStore} from "stores/App";
import {useFlightPlansStore} from "stores/FlightPlans";
const $q = useQuasar();
const $route = useRoute();
@@ -47,9 +48,9 @@ const {airports} = storeToRefs(useAirportsStore());
const {copilots} = storeToRefs(useCopilotsStore());
const {events} = storeToRefs(useEventsStore());
const {photos} = storeToRefs(usePhotosStore());
const {flightPlan, flightPlans} = storeToRefs(useFlightPlansStore());
const {flight, isQueryRunning, isRequestPending: isMutationRunning} = storeToRefs(useFlightsStore());
const formStep = ref("basic");
watch(() => $route.params.step, (val) => formStep.value = val);
@@ -71,6 +72,11 @@ onMounted(() => {
useAirportsStore().fetch();
useCopilotsStore().fetch();
useEventsStore().fetch({});
useFlightPlansStore().fetch();
if (flight.value.flightPlanId) {
useFlightPlansStore().fetchDetail(flight.value.flightPlanId);
}
});
const handleFlightEdit = (data) => {
@@ -234,5 +240,14 @@ const handleBatchPhotoEdit = async (data) => {
<q-tab-panel name="notes">
<FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
</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>
</template>
@@ -20,10 +20,14 @@ import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue";
import FlightPlanSideControls from "components/flightPlans/FlightPlanSideControls.vue";
import {useAircraftsStore} from "stores/Aircrafts";
import {successObject} from "src/composables/dialog";
import {errorObject, successObject} from "src/composables/dialog";
import {useQuasar} from "quasar";
import FlightPlanRouteTable from "components/flightPlans/FlightPlanRouteTable.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 flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán");
@@ -60,6 +64,8 @@ const handleMarkersChanges = (markers) => {
markers: markers.map(m => markerObject({
name: m.name,
type: m.type,
airportId: m.airportId,
pointOfInterestId: m.pointOfInterestId,
gpsLatitude: m.gpsLatitude,
gpsLongitude: m.gpsLongitude,
}))
@@ -75,6 +81,67 @@ const handleFlightPlanInfoSave = (formData) => {
}
const insertAtPosition = ref(flightPlanMarkers.value.length);
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>
<template>
@@ -94,6 +161,11 @@ const fullscreen = ref(false);
Navigační štítek
</q-tab>
<q-tab name="flight" v-if="flightPlan.aircraft">
<q-icon name="flight"/>
Let
</q-tab>
</q-tabs>
<q-tab-panels v-model="openedTab">
<q-tab-panel name="route">
@@ -141,6 +213,16 @@ const fullscreen = ref(false);
:planned-speed="flightPlan.plannedSpeed"
/>
</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-dialog full-width v-model="flightPlannerDialogOpen">
@@ -8,6 +8,7 @@ const $router = useRouter();
onMounted(async () => {
await useFlightPlansStore().create({
name: "",
description: "",
isDefaultName: true,
isPublic: false,
isEditableByCopilot: false,