Zakladni vytvareni letu na zaklade planu
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"])
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ ${FRAGMENT_WEATHER_FULL_INFO}
|
||||
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
|
||||
fragment flightFullPrivateInfo on Flight {
|
||||
...flightFullInfo
|
||||
flightPlanId
|
||||
debriefingNotes
|
||||
|
||||
copilots {
|
||||
|
||||
@@ -7,6 +7,8 @@ fragment flightPlanMarker on FlightPlanMarker {
|
||||
type
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
pointOfInterestId
|
||||
airportId
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user