Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3501e1189f | ||
|
|
d8f11c3862 | ||
|
|
e8dc51a80d | ||
|
|
88a760846c | ||
|
|
a8ad8467ce | ||
|
|
ba1089af3d | ||
|
|
16650cc0cb | ||
|
|
25b1735c87 | ||
|
|
5280f60f34 | ||
|
|
ffbb3ff178 | ||
|
|
e01bafee73 | ||
|
|
852d00b932 | ||
|
|
aa6bed6f7f | ||
|
|
70bc57d3c7 | ||
|
|
32ff47a3a7 | ||
|
|
97d1787642 | ||
|
|
8cc1746be7 | ||
|
|
3c51631bcd |
+5
-3
@@ -1,5 +1,5 @@
|
||||
# develop stage
|
||||
FROM node:20-alpine as develop-stage
|
||||
FROM node:20-alpine AS develop-stage
|
||||
|
||||
ENV TZ=Europe/Prague
|
||||
|
||||
@@ -12,12 +12,14 @@ RUN npx update-browserslist-db@latest
|
||||
COPY . .
|
||||
|
||||
# build stage
|
||||
FROM develop-stage as build-stage
|
||||
FROM develop-stage AS build-stage
|
||||
ENV NODE_ENV production
|
||||
ENV API_URL "https://api.poletani.cz"
|
||||
ENV GOOGLE_MAPS_API_KEY "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
||||
|
||||
RUN quasar build -m ssr
|
||||
|
||||
FROM node:20-alpine as production-stage
|
||||
FROM node:20-alpine AS production-stage
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV TZ=Europe/Prague
|
||||
|
||||
@@ -2,11 +2,6 @@ version: "3.4"
|
||||
services:
|
||||
poletani-app:
|
||||
image: "docker.kvacek.cz/poletani/app:latest"
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# target: "production-stage"
|
||||
restart: always
|
||||
ports:
|
||||
# - 9000:80
|
||||
- 9000:3000
|
||||
|
||||
+2
-35
@@ -1,30 +1,5 @@
|
||||
|
||||
services:
|
||||
#
|
||||
# db:
|
||||
# image: mariadb:10.11
|
||||
# ports:
|
||||
# - 3306:3306
|
||||
# environment:
|
||||
# - MYSQL_USER=root
|
||||
# - MYSQL_DATABASE=ull_tracker
|
||||
# - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
||||
# volumes:
|
||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
# - db-data:/var/lib/mysql:rw
|
||||
# api:
|
||||
# build:
|
||||
# context: ../api
|
||||
# ports:
|
||||
# - 8000:8000
|
||||
# volumes:
|
||||
# - ../api:/app
|
||||
# environment:
|
||||
# TZ: "Europe/Prague"
|
||||
## command: ls -l /app
|
||||
|
||||
app:
|
||||
network_mode: host
|
||||
build:
|
||||
context: .
|
||||
target: "develop-stage"
|
||||
@@ -32,19 +7,11 @@ services:
|
||||
- "9000:9000"
|
||||
- "9001:9100"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./src-ssr:/app/src-ssr
|
||||
- ./public:/app/public
|
||||
# - ./node_modules:/app/node_modules:delegated
|
||||
- ./:/app
|
||||
command: quasar dev # -m ssr
|
||||
environment:
|
||||
API_URL: "http://127.0.0.1:8000"
|
||||
|
||||
# nginx:
|
||||
# image:
|
||||
#
|
||||
#
|
||||
|
||||
GOOGLE_MAPS_API_KEY: "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
||||
|
||||
volumes:
|
||||
db-data: { }
|
||||
|
||||
+8
-5
@@ -12,6 +12,9 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
"@quasar/extras": "^1.15.11",
|
||||
"@sentry/browser": "^8.9.2",
|
||||
"@sentry/integrations": "^7.114.0",
|
||||
"@sentry/vue": "^8.11.0",
|
||||
"@vue/apollo-composable": "4.0.0-beta.2",
|
||||
"@vue/apollo-option": "^4.0.0-beta.5",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
@@ -22,15 +25,15 @@
|
||||
"photoswipe": "^5.4.2",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"pinia": "^2.0.33",
|
||||
"quasar": "^2.14.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"quasar": "2.16.2",
|
||||
"vue": "3.4.27",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-google-map": "^0.15.0"
|
||||
"vue3-google-map": "^0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.7.1",
|
||||
"@quasar/app-vite": "1.9.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
|
||||
+7
-6
@@ -29,6 +29,7 @@ module.exports = configure(function (ctx) {
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||
boot: [
|
||||
"sentry",
|
||||
"cookies",
|
||||
"axios",
|
||||
"apollo",
|
||||
@@ -56,13 +57,13 @@ module.exports = configure(function (ctx) {
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
target: {
|
||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||
node: 'node20'
|
||||
},
|
||||
|
||||
env: {
|
||||
API_URL: ctx.dev ? 'http://localhost:8000' : 'https://api.poletani.cz',
|
||||
GOOGLE_MAPS_API_KEY: "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
||||
API_URL: process.env.API_URL,
|
||||
GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY
|
||||
},
|
||||
|
||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||
@@ -80,8 +81,7 @@ module.exports = configure(function (ctx) {
|
||||
minify: true,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
// extendViteConf(viteConf, {isServer}) {},
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
// vitePlugins: []
|
||||
@@ -140,7 +140,8 @@ module.exports = configure(function (ctx) {
|
||||
ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||
// will mess up SSR
|
||||
|
||||
extendSSRWebserverConf (esbuildConf) {},
|
||||
extendSSRWebserverConf(esbuildConf) {
|
||||
},
|
||||
// extendPackageJson (json) {},
|
||||
|
||||
pwa: false,
|
||||
|
||||
+6
-4
@@ -3,9 +3,8 @@ import {onError} from "@apollo/client/link/error";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {Notify} from "quasar";
|
||||
import {errorObject, warningObject} from "src/composables/dialog";
|
||||
import {createUploadLink} from "apollo-upload-client";
|
||||
import {useAuthStore} from "stores/Auth";
|
||||
import {useRouter} from "vue-router";
|
||||
import {createUploadLink} from "apollo-upload-client";
|
||||
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||
|
||||
const handleRefreshToken = (store, router, forward, operation) => {
|
||||
@@ -14,7 +13,7 @@ const handleRefreshToken = (store, router, forward, operation) => {
|
||||
console.log("refreshing token, ", operation.query);
|
||||
return fromPromise(useAuthStore(store).refreshAccessToken()
|
||||
.catch((error) => {
|
||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||
Notify.create(warningObject("Tvoje přihlášení vypršelo. Přihlaš se prosím znovu."));
|
||||
router.push({name: "login"})
|
||||
}))
|
||||
.filter((value) => Boolean(value))
|
||||
@@ -68,7 +67,10 @@ export default (({store, router}) => {
|
||||
});
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
|
||||
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: true,
|
||||
ssrMode: true,
|
||||
});
|
||||
|
||||
apolloWrapper.query = (query, variables = {}) => {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {boot} from "quasar/wrappers";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
export default boot(({app, router}) => {
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: "https://b2580fb40f4f0037d24f32e89d3e5df2@o472821.ingest.us.sentry.io/4506051015802880",
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration({router}),
|
||||
Sentry.replayIntegration(),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for tracing.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled
|
||||
// tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useAltitudeUtils} from "src/composables/airspaces";
|
||||
|
||||
const props = defineProps({
|
||||
lowerLimit: {
|
||||
@@ -20,12 +21,10 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const getLimitFormatted = (limit, unit) => {
|
||||
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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -36,8 +36,8 @@ useEventsStore().fetchFlightsTracks({
|
||||
for (const flightTrack of data.flights) {
|
||||
totalTime.value += flightTrack.durationMinCalculated;
|
||||
|
||||
if (flightTrack.gpxTrack) {
|
||||
tracks.value.push(flightTrack.gpxTrack.coordinates);
|
||||
if (flightTrack.track) {
|
||||
tracks.value.push(flightTrack.track.coordinates);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "event",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@kvacek.cz:poletani.cz/app.git"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {reactive} from "vue";
|
||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object
|
||||
},
|
||||
aircrafts: {
|
||||
type: Array,
|
||||
},
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const formData = reactive({
|
||||
name: props.flightPlan.name || null,
|
||||
isDefaultName: props.flightPlan ? props.flightPlan.isDefaultName : true,
|
||||
isEditableByCopilot: props.flightPlan.isEditableByCopilot || false,
|
||||
isPublic: props.flightPlan.isPublic || false,
|
||||
plannedTakeoffDatetime: props.flightPlan.plannedTakeoffDatetime || null,
|
||||
aircraft: props.flightPlan && props.flightPlan.aircraft ? {
|
||||
id: props.flightPlan.aircraft.id, name: props.flightPlan.aircraft.callSign
|
||||
} : null,
|
||||
plannedSpeed: props.flightPlan.plannedSpeed || 150,
|
||||
description: props.flightPlan.description || '',
|
||||
});
|
||||
|
||||
const {user} = storeToRefs(useLoggedUserStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData" :is-request-pending="isRequestPending">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-10" v-if="!formData.isDefaultName">
|
||||
<q-input v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-checkbox v-model="formData.isDefaultName" label="Automatické jméno" v-bind="commonProps"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-checkbox v-model="formData.isPublic" label="Zveřejnit"/>
|
||||
<CopyLinkButton
|
||||
v-if="flightPlan.isPublic"
|
||||
:route="{name: 'publicFlightPlanDetail', params: {username: user.publicUsername, id: flightPlan.id}}"
|
||||
/>
|
||||
|
||||
<q-editor v-model="formData.description" label="Popis" v-bind="commonProps"/>
|
||||
<DatetimePicker v-model="formData.plannedTakeoffDatetime" label="Plánovaný datum a čas vzletu" v-bind="commonProps"/>
|
||||
<ComboboxInput v-model="formData.aircraft" :options="aircrafts" label="Letadlo" v-bind="commonProps"/>
|
||||
|
||||
<q-input v-model.number="formData.plannedSpeed" type="number" step="5" label="Plánovaná rychlost" v-bind="commonProps">
|
||||
<template #append>km/h</template>
|
||||
</q-input>
|
||||
</FormWrapper>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {Marker, CustomMarker, Polyline} from "vue3-google-map";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useFlightPlanCalculations, useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -24,15 +24,17 @@ const props = defineProps({
|
||||
},
|
||||
insertPosition: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
const mapWrap = ref(null);
|
||||
|
||||
@@ -40,30 +42,44 @@ const handleMarkerClick = (marker) => {
|
||||
mapWrap.value.openInfoWindow({marker});
|
||||
}
|
||||
|
||||
const handlePoiClick = (poi) => {
|
||||
mapWrap.value.openInfoWindow({poi});
|
||||
}
|
||||
|
||||
const markers = ref([]);
|
||||
watch(() => props.flightPlan.track, (newVal) => {
|
||||
watch(() => props.flightPlan.navlog, (newVal) => {
|
||||
markers.value = getMarkersFrom(props.flightPlan);
|
||||
}, {immediate: true, deep: true});
|
||||
|
||||
const track = computed(() => markers.value.map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||
|
||||
const trackAids = computed(() => props.flightPlan.navlog.map(p => ({
|
||||
...p,
|
||||
anchorPoint: 'CENTER',
|
||||
bearing: p.bearing,
|
||||
distance: p.distance,
|
||||
position: {
|
||||
lat: (p.from.gpsLatitude + p.to?.gpsLatitude) / 2,
|
||||
lng: (p.from.gpsLongitude + p.to?.gpsLongitude) / 2
|
||||
}
|
||||
}
|
||||
)));
|
||||
const displayTrackAids = ref(true);
|
||||
|
||||
const addMarker = async (point) => {
|
||||
if (!point) return;
|
||||
|
||||
const insertAt = Math.min(markers.value.length, props.insertPosition);
|
||||
|
||||
markers.value.splice(insertAt, 0, markerObject({...point}));
|
||||
// infoWindow.value = infoWindowObject({active: false});
|
||||
|
||||
emits("markers-changed", markers.value);
|
||||
emits("change-insert-position", insertAt + 1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const {searchPlaceName} = useGeocode();
|
||||
const handleMarkerMove = async (idx, event) => {
|
||||
markers.value[idx].name = await searchPlaceName(event.lat(), event.lng());
|
||||
@@ -76,33 +92,26 @@ const handleMarkerMove = async (idx, event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<MapWrapper
|
||||
ref="mapWrap"
|
||||
style="width: 100%; height: 100%; min-height: 500px"
|
||||
style="width: 100%; height: 65vh; 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"
|
||||
:fullscreen-control="false"
|
||||
:show-info-window="true"
|
||||
:show-layer-controls="true"
|
||||
:allow-add-marker="true"
|
||||
:allow-add-marker="insertPosition !== null"
|
||||
@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">
|
||||
<!-- TODO: ve verejne casti by nemel byt draggable!-->
|
||||
<Marker
|
||||
v-if="editableNameTypes.includes(marker.type)"
|
||||
:options="googleMapsMarkerObject(marker)"
|
||||
@@ -110,6 +119,65 @@ const handleMarkerMove = async (idx, event) => {
|
||||
@dragend="handleMarkerMove(i, $event.latLng)"
|
||||
/>
|
||||
</template>
|
||||
<Polyline :options="{path: track, geodesic: true, strokeColor: '#ff00ff', strokeOpacity: 1, strokeWeight: 3}"/>
|
||||
|
||||
<template v-if="displayTrackAids">
|
||||
<template v-for="marker in trackAids" :key="marker">
|
||||
<CustomMarker :options="marker" class="track-info-marker">
|
||||
<div class="track-info" :style='`transform: rotate(${marker.bearing}deg)`'>
|
||||
{{ marker.bearing }}° <br>
|
||||
{{ getDuration(marker.distance, flightPlan.plannedSpeed) }}'
|
||||
</div>
|
||||
</CustomMarker>
|
||||
</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 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;
|
||||
border: solid 2px rgba(0, 0, 0, 0.87);
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.track-info-marker {
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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}));
|
||||
});
|
||||
@@ -116,4 +115,5 @@ const allPlacesOptions = computed(() => {
|
||||
:options="allPlacesOptions"
|
||||
@select="handlePlaceSelect"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useFlightPlanCalculations} from "src/composables/flightPlan";
|
||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {useQuasar} from "quasar";
|
||||
import {textEllipsis} from "src/composables/utils";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
plannedSpeed: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
// name: 'from',
|
||||
label: 'Z',
|
||||
align: "left",
|
||||
field: 'from'
|
||||
},
|
||||
{
|
||||
label: 'Do',
|
||||
align: "left",
|
||||
field: 'to'
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
label: 'Kurz',
|
||||
field: 'bearing',
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
label: 'Vzdálenost',
|
||||
field: 'distance',
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
label: 'Čas',
|
||||
field: 'duration',
|
||||
},
|
||||
];
|
||||
|
||||
const {getDuration, getTotalDuration, getTotalDistance} = useFlightPlanCalculations();
|
||||
|
||||
const data = computed(() => props.flightPlan.navlog.map((item) => ({
|
||||
from: textEllipsis(item.from.name, 30),
|
||||
to: textEllipsis(item.to.name, 30),
|
||||
distance: `${item.distance} km`,
|
||||
bearing: `${item.bearing} °`,
|
||||
duration: minutesToHumanReadable(getDuration(item.distance, props.plannedSpeed)),
|
||||
})));
|
||||
|
||||
const {screen} = useQuasar();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-table
|
||||
:grid="screen.xs"
|
||||
:dense="screen.lt.md"
|
||||
flat bordered
|
||||
hide-bottom
|
||||
:title="flightPlan.name"
|
||||
:rows="data"
|
||||
:columns="columns"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template #bottom-row="{cols}">
|
||||
<tr class="summary">
|
||||
<td v-for="col in cols" :key="col.field" :class="{'text-right': col.align === 'right'}">
|
||||
<template v-if="col.field === 'duration'">
|
||||
{{ minutesToHumanReadable(getTotalDuration(flightPlan, plannedSpeed)) }}
|
||||
</template>
|
||||
<template v-else-if="col.field === 'distance'">
|
||||
{{ getTotalDistance(flightPlan) }} km
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,13 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useFlightPlanCalculations, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
plannedSpeed: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 150
|
||||
},
|
||||
isFullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -23,28 +18,14 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["open", "change-insert-position", "fullscren-toggle"])
|
||||
|
||||
|
||||
const getDuration = (distance, speed) => Math.round(distance / speed * 60)
|
||||
|
||||
const totalDistance = computed(() => {
|
||||
if (!props.flightPlan.track.length) return null;
|
||||
|
||||
return Math.round(props.flightPlan.track.map(t => t.distance).reduce((a, b) => a + b));
|
||||
});
|
||||
const totalTime = computed(() => {
|
||||
if (!props.flightPlan.track.length) return null;
|
||||
|
||||
return props.flightPlan.track.map(t => getDuration(t.distance, props.plannedSpeed)).reduce((a, b) => a + b)
|
||||
});
|
||||
|
||||
const emits = defineEmits(["open", "change-insert-position", "fullscreen-toggle"])
|
||||
const {getDuration, getTotalDistance, getTotalDuration} = useFlightPlanCalculations();
|
||||
const totalDistance = computed(() => getTotalDistance(props.flightPlan));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list bordered class="q-ml-md-md q-pt-md">
|
||||
|
||||
<q-item>
|
||||
<q-item class="mobile-hide">
|
||||
<q-item-section>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<q-btn
|
||||
@@ -59,39 +40,37 @@ const totalTime = computed(() => {
|
||||
<q-btn
|
||||
@click='emits("fullscreen-toggle")'
|
||||
size="sm"
|
||||
round
|
||||
:icon="isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-scroll-area style="height: 50vh">
|
||||
<template v-for="(segment, i) in props.flightPlan.track" :key="segment">
|
||||
<q-scroll-area class="mobile-hide" style="height: 55vh">
|
||||
<template v-for="(segment, i) in props.flightPlan.navlog" :key="segment">
|
||||
<q-item dense>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ segment.from.name }}</q-item-label>
|
||||
<q-item-label caption class="q-py-sm" v-if="segment.distance">
|
||||
<q-item-label caption class="q-pa-sm q-ml-sm" style="border-left: solid 1px" v-if="segment.distance">
|
||||
|
||||
{{ segment.bearing }}°,
|
||||
{{ segment.distance }} km,
|
||||
{{ minutesToHumanReadable(getDuration(segment.distance, props.plannedSpeed)) }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ minutesToHumanReadable(getDuration(segment.distance, props.flightPlan.plannedSpeed)) }}
|
||||
|
||||
<q-btn
|
||||
@click="emits('change-insert-position', i+1)"
|
||||
icon="add"
|
||||
size="sm"
|
||||
class="q-ml-lg q-mb-sm"
|
||||
:round="insertPosition !== i+1"
|
||||
size="xs"
|
||||
round
|
||||
class="q-ml-sm"
|
||||
:color="insertPosition === i+1 ? 'primary' : 'grey'"
|
||||
>
|
||||
<template v-if="insertPosition === i+1">Přidávám sem</template>
|
||||
</q-btn>
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
|
||||
<q-item dense v-if="segment.to && i === props.flightPlan.track.length - 1">
|
||||
<q-item dense v-if="segment.to && i === props.flightPlan.navlog.length - 1">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ segment.to.name }}
|
||||
@@ -99,23 +78,24 @@ const totalTime = computed(() => {
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-scroll-area>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-section v-if="flightPlan.navlog.length">
|
||||
<div>
|
||||
<q-btn
|
||||
@click="emits('change-insert-position', flightPlan.track.length + 1)"
|
||||
@click="emits('change-insert-position', flightPlan.navlog.length + 1)"
|
||||
icon="add"
|
||||
size="sm"
|
||||
:color="insertPosition === flightPlan.track.length + 1? 'primary' : 'grey'"
|
||||
:color="insertPosition === flightPlan.navlog.length + 1? 'primary' : 'grey'"
|
||||
>
|
||||
<template v-if="insertPosition === flightPlan.track.length + 1">Přidávám na konec</template>
|
||||
<template v-if="insertPosition === flightPlan.navlog.length + 1">Přidávám na konec</template>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-scroll-area>
|
||||
|
||||
<template v-if="totalDistance">
|
||||
<q-item>
|
||||
<q-item class="mobile-hide">
|
||||
<q-item-section>
|
||||
<q-separator/>
|
||||
</q-item-section>
|
||||
@@ -124,7 +104,7 @@ const totalTime = computed(() => {
|
||||
<q-item-section>
|
||||
Celková vzdálenost: {{ totalDistance }} km
|
||||
<br>
|
||||
Plánovaná doba letu: {{ minutesToHumanReadable(totalTime) }}
|
||||
Plánovaná doba letu: {{ minutesToHumanReadable(getTotalDuration(props.flightPlan)) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Line :data="chartAltitudeData" :options="chartOptions"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {Line} from "vue-chartjs";
|
||||
|
||||
const props = defineProps({
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const timestamp = props.flightPlan.track.points.map(i => i.timestamp);
|
||||
const terrainElevation = props.flightPlan.track.points.map(i => i.terrainElevation || 0);
|
||||
|
||||
|
||||
const chartOptions = {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
const chartLabels = timestamp.map(t => {
|
||||
// TODO: zakomponovat i nazvy otocnych bodu
|
||||
const datetime = new Date(t);
|
||||
return datetime.getHours() + ":" + datetime.getMinutes();
|
||||
});
|
||||
|
||||
|
||||
const chartAltitudeData = {
|
||||
labels: chartLabels,
|
||||
datasets: [{
|
||||
backgroundColor: "rgba(148,94,40,0.7)",
|
||||
borderColor: "#945e28",
|
||||
label: "Nadmořská výška země (m)",
|
||||
data: terrainElevation,
|
||||
fill: true,
|
||||
pointRadius: 1,
|
||||
}]
|
||||
};
|
||||
</script>
|
||||
@@ -82,7 +82,7 @@ const props = defineProps({
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item v-for="poi in flight.track" :key="poi.id">
|
||||
<q-item v-for="poi in flight.turnPoints" :key="poi.id">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="place" v-if="poi.pointOfInterest"/>
|
||||
<q-icon name="local_airport" v-if="poi.airport"/>
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {Line} from "vue-chartjs"
|
||||
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
flightPlan: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
photos: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -16,7 +21,7 @@ const props = defineProps({
|
||||
|
||||
|
||||
const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
|
||||
const poisWithGPS = computed(() => props.flight.turnPoints.map(poi => poi.pointOfInterest || poi.airport));
|
||||
|
||||
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||
const pointsOfInterests = computed(() => mapMarkers
|
||||
@@ -25,10 +30,12 @@ const pointsOfInterests = computed(() => mapMarkers
|
||||
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||
));
|
||||
|
||||
const gpxTrack = props.flight.gpxTrack ? reactive(props.flight.gpxTrack) : null;
|
||||
// Tuu bylo reactive()
|
||||
const flightTrack = props.flight.track ? props.flight.track.points : null;
|
||||
|
||||
const displayMap = computed(() => {
|
||||
return (
|
||||
!!gpxTrack ||
|
||||
!!flightTrack ||
|
||||
mapPhotos.value.length > 0 ||
|
||||
poisWithGPS.value.length > 0 ||
|
||||
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
||||
@@ -36,8 +43,14 @@ const displayMap = computed(() => {
|
||||
});
|
||||
|
||||
let chartSpeedData, chartAltitudeData = null;
|
||||
if (gpxTrack) {
|
||||
const chartLabels = gpxTrack.time.map(t => {
|
||||
if (flightTrack) {
|
||||
|
||||
const timestamp = flightTrack.map(i => i.timestamp);
|
||||
const speed = flightTrack.map(i => i.speed);
|
||||
const altitude = flightTrack.map(i => i.altitude);
|
||||
const terrainElevation = flightTrack.map(i => i.terrainElevation || 0);
|
||||
|
||||
const chartLabels = timestamp.map(t => {
|
||||
const datetime = new Date(t);
|
||||
return datetime.getHours() + ":" + datetime.getMinutes();
|
||||
});
|
||||
@@ -47,7 +60,7 @@ if (gpxTrack) {
|
||||
backgroundColor: "#38bd3e",
|
||||
borderColor: "#38bd3e",
|
||||
label: "Rychlost (km/h)",
|
||||
data: gpxTrack.speed,
|
||||
data: speed,
|
||||
pointRadius: 1,
|
||||
}]
|
||||
};
|
||||
@@ -58,18 +71,18 @@ if (gpxTrack) {
|
||||
backgroundColor: "#9ae3ff",
|
||||
borderColor: "#9ae3ff",
|
||||
label: "Nadmořská výška letu (m)",
|
||||
data: gpxTrack.altitude,
|
||||
data: altitude,
|
||||
pointRadius: 1,
|
||||
}]
|
||||
};
|
||||
|
||||
if (gpxTrack.terrainElevation.length > 0) {
|
||||
if (terrainElevation.length > 0) {
|
||||
chartAltitudeData.datasets.push(
|
||||
{
|
||||
backgroundColor: "rgba(148,94,40,0.7)",
|
||||
borderColor: "#945e28",
|
||||
label: "Nadmořská výška země (m)",
|
||||
data: gpxTrack.terrainElevation,
|
||||
data: terrainElevation,
|
||||
fill: true,
|
||||
pointRadius: 1,
|
||||
});
|
||||
@@ -85,7 +98,7 @@ const drawCurrentPositionMarker = (e) => {
|
||||
return;
|
||||
}
|
||||
const idx = res[0].index;
|
||||
currentPositionMarker.value = props.flight.gpxTrack.coordinates[idx];
|
||||
currentPositionMarker.value = props.flight.track.points[idx].coordinates;
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
@@ -98,23 +111,38 @@ const chartOptions = {
|
||||
onClick: drawCurrentPositionMarker,
|
||||
onHover: drawCurrentPositionMarker,
|
||||
}
|
||||
|
||||
const {getMarkersFrom} = useFlightPlanUtils();
|
||||
const showPlanTrack = ref(true);
|
||||
const tracks = computed(() => {
|
||||
const items = [];
|
||||
if (props.flight.track) {
|
||||
items.push(props.flight.track.points.map(m => m.coordinates))
|
||||
}
|
||||
if (showPlanTrack.value && props.flightPlan) {
|
||||
items.push(getMarkersFrom(props.flightPlan).map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||
}
|
||||
return items;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-no-ssr>
|
||||
<q-intersection>
|
||||
<q-checkbox v-model="showPlanTrack" label="Zobrazovat plán?" v-if="flightPlan" />
|
||||
|
||||
<MapWithMarkers
|
||||
v-if="displayMap"
|
||||
style="width: 100%; height: 500px;"
|
||||
:gpx-track="flight.gpxTrack?.coordinates || []"
|
||||
:tracks="tracks"
|
||||
:markers="pointsOfInterests"
|
||||
:highlight-position="currentPositionMarker"
|
||||
:photos="mapPhotos"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<Line v-if="gpxTrack" :data="chartSpeedData" :options="chartOptions"/>
|
||||
<Line v-if="gpxTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
||||
<Line v-if="flightTrack" :data="chartSpeedData" :options="chartOptions"/>
|
||||
<Line v-if="flightTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
||||
</q-intersection>
|
||||
</q-no-ssr>
|
||||
</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"])
|
||||
|
||||
@@ -51,7 +51,7 @@ watch(poi, (newVal) => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.flight.track, (newVal) => {
|
||||
watch(() => props.flight.turnPoints, (newVal) => {
|
||||
formData.track = [];
|
||||
for (const i in newVal) {
|
||||
const trackInfo = newVal[i];
|
||||
@@ -76,7 +76,7 @@ watch(() => props.flight.track, (newVal) => {
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<q-banner class="bg-green text-white" v-if="flight.gpxTrack">
|
||||
<q-banner class="bg-green text-white" v-if="flight.track">
|
||||
<q-icon name="check"/>
|
||||
Trasa nahrána
|
||||
</q-banner>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import {InfoWindow} from "vue3-google-map";
|
||||
import {CustomMarker, InfoWindow} from "vue3-google-map";
|
||||
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
import {ref, watch} from "vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useAirspaceColors} from "src/composables/airspaces";
|
||||
import AltitudeLimit from "components/AltitudeLimit.vue";
|
||||
|
||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
isInTrack: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -23,50 +28,126 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["add-to-route", "highlight-airspace", "close"])
|
||||
const emits = defineEmits(["add-to-route", "remove-from-route", "highlight-airspace", "close"])
|
||||
|
||||
const {getIcon} = useFlightPlanUtils();
|
||||
const {searchPlaceName} = useGeocode()
|
||||
|
||||
const title = ref(null);
|
||||
|
||||
const isOpened = ref(props.infoWindow.active);
|
||||
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});
|
||||
|
||||
const addPoint = async () => {
|
||||
const addPoint = () => {
|
||||
let type = "mapPoint";
|
||||
if (props.infoWindow.poi) {
|
||||
type = "poi";
|
||||
} 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
|
||||
}
|
||||
emits("add-to-route", point)
|
||||
closeInfoWindow();
|
||||
}
|
||||
|
||||
const closeInfoWindow = () => {
|
||||
emits('close');
|
||||
isOpened.value = false;
|
||||
}
|
||||
|
||||
const removePoint = () => {
|
||||
emits("remove-from-route", props.infoWindow);
|
||||
emits("close");
|
||||
}
|
||||
|
||||
const {getColorByType} = useAirspaceColors();
|
||||
const highlightAirspace = (airspace) => {
|
||||
emits('highlight-airspace', airspace.name)
|
||||
}
|
||||
|
||||
const runways = computed(() => {
|
||||
const runwayPairs = {};
|
||||
|
||||
for (const runway of props.infoWindow.airport.runways) {
|
||||
runwayPairs[runway.designator] = runway;
|
||||
}
|
||||
|
||||
return runwayPairs;
|
||||
});
|
||||
|
||||
const runwaySurfaceName = (surface) => {
|
||||
const surfaceMap = {
|
||||
grass: "tráva",
|
||||
concrete: "beton",
|
||||
asphalt: "asfalt"
|
||||
}
|
||||
return surfaceMap[surface] || 'jiné'
|
||||
}
|
||||
|
||||
const openedTab = ref("info");
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<InfoWindow
|
||||
@close="emits('close')"
|
||||
@close="closeInfoWindow"
|
||||
v-model="isOpened"
|
||||
:options="{position: infoWindow.position}"
|
||||
>
|
||||
<div class="text-h5">{{ title }}</div>
|
||||
<q-tabs v-model="openedTab">
|
||||
<q-tab name="info">{{ title }}</q-tab>
|
||||
<q-tab name="airspaces" v-if="infoWindow.airspaces?.length">Prostory</q-tab>
|
||||
</q-tabs>
|
||||
<q-tab-panels v-model="openedTab">
|
||||
<q-tab-panel name="info">
|
||||
<div v-if="infoWindow.airport">
|
||||
Elevace: {{ infoWindow.airport.elevation }} m
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-list dense>
|
||||
<q-item dense v-for="runway in runways" :key="runway">
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>{{ runway.designator }}</strong> ({{ runwaySurfaceName(runway.surface) }})
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ runway.heading }}°, {{ runway.width }} × {{ runway.length }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item dense v-for="frequency in infoWindow.airport.frequencies" :key="frequency">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>{{ frequency.name }}</strong>
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ frequency.frequency }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="infoWindow.marker">
|
||||
<span class="text-h6">
|
||||
@@ -78,9 +159,8 @@ const highlightAirspace = (airspace) => {
|
||||
<div v-if="infoWindow.poi">
|
||||
<q-img :src="infoWindow.poi.titlePhoto.thumbnailUrl" width="300px" v-if="infoWindow.poi.titlePhoto"/>
|
||||
</div>
|
||||
|
||||
<template v-if="infoWindow.airspaces?.length">
|
||||
<span class="text-h6">Prostory</span>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="airspaces" v-if="infoWindow.airspaces?.length">
|
||||
<q-list>
|
||||
<q-item-label v-for="airspace in infoWindow.airspaces" :key="airspace.name">
|
||||
<q-avatar size="16px" class="q-mx-sm" :style="`border: solid 1px; background: ${getColorByType(airspace.type)}`"/>
|
||||
@@ -109,13 +189,41 @@ const highlightAirspace = (airspace) => {
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-list>
|
||||
</template>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<template v-if="showActions">
|
||||
<q-btn
|
||||
v-if="showActions"
|
||||
class="q-mt-lg"
|
||||
v-if="isInTrack"
|
||||
class="q-mr-sm q-mt-lg"
|
||||
color="warning"
|
||||
@click="removePoint()"
|
||||
>
|
||||
Odebrat z trasy
|
||||
</q-btn>
|
||||
<q-btn
|
||||
:class="{'q-mt-lg': true, 'full-width': !isInTrack}"
|
||||
color="secondary"
|
||||
@click="addPoint()">
|
||||
@click="addPoint()"
|
||||
>
|
||||
Přidat bod do trasy
|
||||
</q-btn>
|
||||
</InfoWindow>
|
||||
</template>
|
||||
|
||||
<CustomMarker :options="{position: infoWindow.position, anchorPoint: 'CENTER'}" v-if="isOpened">
|
||||
<div class="info-window-position"></div>
|
||||
</CustomMarker>
|
||||
</InfoWindow>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.info-window-position {
|
||||
border: solid 4px orange;
|
||||
outline: solid 1px black;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,12 +5,6 @@ import {computed} from "vue";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
gpxTrack: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
}
|
||||
},
|
||||
tracks: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -29,6 +23,11 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
bounds: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -36,14 +35,15 @@ const bounds = computed(() => {
|
||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||
const coordinates = [
|
||||
...[...props.markers, ...photosTransformed].filter(i => !!i.position).map(i => i.position),
|
||||
...(props.gpxTrack || []),
|
||||
...(props.tracks || []).flat()
|
||||
];
|
||||
|
||||
const minLat = Math.min(...coordinates.map(i => i.lat));
|
||||
const maxLat = Math.max(...coordinates.map(i => i.lat));
|
||||
const minLng = Math.min(...coordinates.map(i => i.lng));
|
||||
const maxLng = Math.max(...coordinates.map(i => i.lng));
|
||||
const latitudes = coordinates.filter(i => i && i.lat).map(i => i.lat);
|
||||
const longitudes = coordinates.filter(i => i && i.lng).map(i => i.lng);
|
||||
const minLat = Math.min(...latitudes);
|
||||
const maxLat = Math.max(...latitudes);
|
||||
const minLng = Math.min(...longitudes);
|
||||
const maxLng = Math.max(...longitudes);
|
||||
|
||||
return [
|
||||
{lat: minLat, lng: minLng},
|
||||
@@ -84,10 +84,6 @@ const getColor = (i) => {
|
||||
</InfoWindow>
|
||||
</Marker>
|
||||
|
||||
<template v-if="gpxTrack && gpxTrack.length > 0">
|
||||
<Polyline :options="{path: gpxTrack, geodesic: true, strokeColor: getColor(0), strokeOpacity: 0.5, strokeWeight: 5}"/>
|
||||
</template>
|
||||
|
||||
<template v-if="tracks && tracks.length > 0">
|
||||
<template v-for="(track, i) in tracks" :key="track">
|
||||
<Polyline :options="{path: track, geodesic: true, strokeColor: getColor(i), strokeOpacity: 0.5, strokeWeight: 5}"/>
|
||||
|
||||
@@ -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,
|
||||
@@ -38,12 +48,13 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["map-click", "add-to-route"]);
|
||||
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,48 +69,70 @@ 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))
|
||||
fillOpacity = 0.1;
|
||||
if (highlightedAirspaces.value.includes(airspace.name))
|
||||
fillOpacity = 0.2;
|
||||
if (highlightedAirspaces.value.includes(airspace.name)) {
|
||||
strokeWeight = 2;
|
||||
fillOpacity = 0.3;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: airspace.name,
|
||||
strokeColor: colorsMap[airspace.type],
|
||||
fillColor: colorsMap[airspace.type],
|
||||
strokeOpacity: 0.5,
|
||||
strokeWeight: 1,
|
||||
strokeWeight,
|
||||
fillOpacity,
|
||||
};
|
||||
|
||||
@@ -112,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();
|
||||
@@ -130,7 +169,7 @@ const getAirspacesAtCoordinates = (clickCoordinates) => {
|
||||
}
|
||||
|
||||
const {infoWindowObject} = useFlightPlanObjects();
|
||||
const infoWindow = ref(infoWindowObject({}));
|
||||
const infoWindow = ref({...infoWindowObject({})});
|
||||
|
||||
const openInfoWindow = (data) => {
|
||||
infoWindow.value = infoWindowObject({active: true, ...data});
|
||||
@@ -141,11 +180,20 @@ defineExpose({
|
||||
});
|
||||
|
||||
const emitMapClick = (data) => {
|
||||
isControlsOpened.value = false;
|
||||
openInfoWindow(data);
|
||||
emits("map-click", data);
|
||||
}
|
||||
|
||||
const highlightedAirspaces = ref([]);
|
||||
const highlightAirspace = (airspaceName) => {
|
||||
const airspace = props.airspacesData.find(a => a.name === airspaceName);
|
||||
|
||||
if (airspace) {
|
||||
highlightedAirspaces.value = [airspaceName];
|
||||
airspaceLayers.value[airspace.type].displayed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const handleMapClick = (e) => {
|
||||
const position = {lat: e.latLng.lat(), lng: e.latLng.lng()};
|
||||
@@ -173,6 +221,23 @@ 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(() => {
|
||||
if (!props.track) return false;
|
||||
|
||||
return !!props.track.path.find(i => i.lat === infoWindow.value.position.lat && i.lng === infoWindow.value.position.lng);
|
||||
});
|
||||
|
||||
const isControlsOpened = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -182,17 +247,21 @@ const handleAirportClick = (airport) => {
|
||||
:street-view-control="false"
|
||||
@click="handleMapClick"
|
||||
map-type-id="roadmap"
|
||||
gesture-handling="greedy"
|
||||
:max-zoom="13"
|
||||
ref="mapRef"
|
||||
>
|
||||
<!-- TODO: pridavani a odebirani z trasy resit pres slot-->
|
||||
<AirspaceInfoWindow
|
||||
v-if="showInfoWindow && infoWindow.active"
|
||||
: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)"
|
||||
@highlight-airspace="highlightedAirspaces = [$event]"
|
||||
@remove-from-route="emits('remove-from-route', $event)"
|
||||
@highlight-airspace="highlightAirspace"
|
||||
/>
|
||||
|
||||
<template v-for="airspace in airspaces" :key="airspace.name">
|
||||
@@ -203,40 +272,115 @@ const handleAirportClick = (airport) => {
|
||||
|
||||
<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)"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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 class="poi-avatar" :title="poi.name" v-if="!poi.titlePhoto">
|
||||
<img src="/mapIcons/photo-32x32.png" alt="">
|
||||
</div>
|
||||
|
||||
<div
|
||||
:title="poi.name"
|
||||
v-else
|
||||
class="poi-img"
|
||||
:style="`background-image: url(${poi.titlePhoto.thumbnailUrl})`"
|
||||
>
|
||||
</div>
|
||||
</CustomMarker>
|
||||
</template>
|
||||
|
||||
<Polyline v-if="track" :options="track"/>
|
||||
|
||||
<CustomControl position="TOP_LEFT" v-if="showLayerControls" style="width: auto; max-width: 200px">
|
||||
<q-btn
|
||||
@click="isControlsOpened = !isControlsOpened"
|
||||
:icon="isControlsOpened ? 'layers_clear' : 'layers'"
|
||||
color="grey-2"
|
||||
text-color="grey-9"
|
||||
round
|
||||
size="sm"
|
||||
class="q-my-md"
|
||||
>
|
||||
</q-btn>
|
||||
<q-card
|
||||
v-if="isControlsOpened"
|
||||
class="q-px-lg"
|
||||
style="max-width: 30%; min-width: 200px;"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div class="q-my-lg">
|
||||
Zobrazit prostory od GND do výšky {{ altitudeLimitFormatted(maxAltitude, 'ft') }}
|
||||
<q-slider
|
||||
v-model.number="maxAltitude"
|
||||
:min="1000"
|
||||
:max="15000"
|
||||
label
|
||||
:label-value="altitudeLimitFormatted(maxAltitude, 'ft')"
|
||||
:step="1000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot name="layersControlsEnd"></slot>
|
||||
|
||||
<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"/>
|
||||
</q-card>
|
||||
</CustomControl>
|
||||
|
||||
</GoogleMap>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.poi-img {
|
||||
border: solid 3px #8bdfff;
|
||||
outline: solid 1px black;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.poi-avatar {
|
||||
padding: 5px;
|
||||
border-radius: 15px;
|
||||
background: #8bdfff;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,7 @@ const getRoute = (item) => {
|
||||
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
||||
<template #items>
|
||||
<q-list bordered separator>
|
||||
<q-item clickable v-ripple v-for="poi of items" :key="poi" :to="getRoute(poi)">
|
||||
<q-item clickable v-for="poi of items" :key="poi" :to="getRoute(poi)">
|
||||
<q-item-section avatar>
|
||||
<q-avatar v-if="poi.titlePhoto">
|
||||
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ export const publicCopilotBreadcrumbs = (pilot, flight, event, copilot) => {
|
||||
export const publicPOIBaseBreadcrumbs = () => {
|
||||
return [
|
||||
...homepageBreadcrumbs(),
|
||||
{label: "Místa"},
|
||||
{
|
||||
to: {name: 'publicPOIsList'},
|
||||
label: "Místa"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const flightName = (flight) => {
|
||||
if (flight.name !== '') {
|
||||
return `${flight.name} (${date})`
|
||||
} else {
|
||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||
const track = flight.turnPoints ? flight.turnPoints.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||
|
||||
return `${flight.takeoffAirport?.icaoCode || '???'} ${trackStr} ${flight.landingAirport?.icaoCode || "???"} (${date})`;
|
||||
|
||||
@@ -10,11 +10,11 @@ export const useFlightPlanUtils = () => {
|
||||
}
|
||||
|
||||
const getMarkersFrom = (flightPlan) => {
|
||||
const segments = flightPlan.track.length;
|
||||
const segments = flightPlan.navlog.length;
|
||||
|
||||
const markers = flightPlan.track.map(m => m.from);
|
||||
if (segments > 0 && flightPlan.track[segments - 1].to) {
|
||||
markers.push(flightPlan.track[segments - 1].to);
|
||||
const markers = flightPlan.navlog.map(m => m.from);
|
||||
if (segments > 0 && flightPlan.navlog[segments - 1].to) {
|
||||
markers.push(flightPlan.navlog[segments - 1].to);
|
||||
}
|
||||
|
||||
return markers;
|
||||
@@ -23,14 +23,33 @@ export const useFlightPlanUtils = () => {
|
||||
return {getIcon, getMarkersFrom}
|
||||
}
|
||||
|
||||
export const useFlightPlanCalculations = () => {
|
||||
const getDuration = (distance, speed) => Math.round(distance / speed * 60)
|
||||
|
||||
const getTotalDistance = (flightPlan) => {
|
||||
if (!flightPlan.navlog.length) return null;
|
||||
|
||||
return Math.round(flightPlan.navlog.map(t => t.distance).reduce((a, b) => a + b));
|
||||
}
|
||||
|
||||
const getTotalDuration = (flightPlan, plannedSpeed = undefined) => {
|
||||
if (!plannedSpeed) plannedSpeed = flightPlan.plannedSpeed;
|
||||
if (!flightPlan.navlog.length) return null;
|
||||
|
||||
return flightPlan.navlog.map(t => getDuration(t.distance, plannedSpeed)).reduce((a, b) => a + b)
|
||||
}
|
||||
|
||||
return {getDuration, getTotalDistance, getTotalDuration}
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -39,7 +58,7 @@ export const useFlightPlanObjects = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
anchorPoint: anchorPoints[marker.type],
|
||||
anchorPoint: overrideAnchorPoint || anchorPoints[marker.type],
|
||||
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +71,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 +102,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,3 +8,18 @@ export const pluralize = (count, singular, plural, pluralMore) => {
|
||||
else if (count < 5) return plural;
|
||||
else return pluralMore;
|
||||
}
|
||||
|
||||
export const textEllipsis = (text, maxLength) => {
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let words = text.split(' ');
|
||||
|
||||
const maxWords = 2;
|
||||
if (words.length <= maxWords + 1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return words[0] + ' ... ' + words[words.length - 1];
|
||||
}
|
||||
|
||||
@@ -7,11 +7,24 @@ export const FRAGMENT_AIRPORT_INFO = gql`
|
||||
icaoCode
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
elevation
|
||||
iconUrl
|
||||
reportingPoints {
|
||||
id
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
name
|
||||
}
|
||||
frequencies {
|
||||
frequency
|
||||
name
|
||||
}
|
||||
runways {
|
||||
designator
|
||||
heading
|
||||
surface
|
||||
length
|
||||
width
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -29,7 +29,9 @@ fragment flightFullInfo on Flight {
|
||||
landings
|
||||
durationMinCalculated
|
||||
isPublic
|
||||
flightPlanId
|
||||
|
||||
socialImageUrl
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
url
|
||||
@@ -59,23 +61,30 @@ fragment flightFullInfo on Flight {
|
||||
name
|
||||
}
|
||||
|
||||
gpxTrack {
|
||||
track {
|
||||
points {
|
||||
coordinates {
|
||||
lat
|
||||
lng
|
||||
}
|
||||
time
|
||||
timestamp
|
||||
magneticVariation
|
||||
speed
|
||||
maxSpeed
|
||||
avgSpeed
|
||||
altitude
|
||||
terrainElevation
|
||||
}
|
||||
mapBounds {
|
||||
lat
|
||||
lng
|
||||
}
|
||||
|
||||
maxSpeed
|
||||
avgSpeed
|
||||
maxAltitude
|
||||
avgAltitude
|
||||
}
|
||||
|
||||
track {
|
||||
turnPoints {
|
||||
pointOfInterest {
|
||||
...pointOfInterestFullInfo
|
||||
}
|
||||
@@ -99,6 +108,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
|
||||
}
|
||||
`
|
||||
|
||||
@@ -19,13 +21,18 @@ fragment flightPlanFullInfo on FlightPlan {
|
||||
isDefaultName
|
||||
isPublic
|
||||
isEditableByCopilot
|
||||
description
|
||||
|
||||
aircraft {
|
||||
id
|
||||
callSign
|
||||
seats
|
||||
isPublic
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
}
|
||||
track {
|
||||
}
|
||||
navlog {
|
||||
bearing
|
||||
distance
|
||||
from {
|
||||
@@ -35,6 +42,15 @@ fragment flightPlanFullInfo on FlightPlan {
|
||||
...flightPlanMarker
|
||||
}
|
||||
}
|
||||
|
||||
track {
|
||||
points {
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
timestamp
|
||||
terrainElevation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_FLIGHT_PLAN_MARKER}
|
||||
|
||||
@@ -17,7 +17,7 @@ fragment flightListInfo on Flight {
|
||||
url
|
||||
}
|
||||
|
||||
track {
|
||||
turnPoints {
|
||||
pointOfInterest {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -7,6 +7,7 @@ export const FRAGMENT_USER_FULL_INFO = gql`
|
||||
email
|
||||
avatarImageUrl
|
||||
titleImageUrl
|
||||
socialImageUrl
|
||||
publicUsername
|
||||
description
|
||||
createdAt
|
||||
|
||||
@@ -25,7 +25,7 @@ query eventsFlightsTracks($eventId: Int, $eventUrlSlug: String, $onlyPublic: Boo
|
||||
event(id: $eventId, urlSlug: $eventUrlSlug, public: $onlyPublic) {
|
||||
flights(onlyPublic: $onlyPublic) {
|
||||
durationMinCalculated
|
||||
gpxTrack {
|
||||
tracks {
|
||||
coordinates {
|
||||
lat
|
||||
lng
|
||||
|
||||
@@ -12,8 +12,8 @@ ${FRAGMENT_FLIGHT_PLAN_LIST_INFO}
|
||||
`;
|
||||
|
||||
export const FLIGHT_PLAN_DETAIL = gql`
|
||||
query flightPlansDetail($id: Int!) {
|
||||
flightPlan(id: $id) {
|
||||
query flightPlansDetail($id: Int!, $username: String, $public: Boolean) {
|
||||
flightPlan(id: $id, username: $username, public: $public) {
|
||||
...flightPlanFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
@@ -112,7 +118,7 @@ const openBatchPhotoForm = (photos) => {
|
||||
editBatchPhotos.value = photos;
|
||||
}
|
||||
|
||||
const handleBatchPhotoEdit = async (flightId, data) => {
|
||||
const handleBatchPhotoEdit = async (data) => {
|
||||
const photoIds = data.photoIds;
|
||||
delete data.photoIds;
|
||||
delete data.id;
|
||||
@@ -122,6 +128,7 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
||||
...data.pointOfInterest,
|
||||
isPublic: false,
|
||||
description: "",
|
||||
urlSlug: "",
|
||||
}
|
||||
await usePointsOfInterestStore().create(poiData).then(poi => data.pointOfInterest = {id: poi.id, name: poi.name});
|
||||
}
|
||||
@@ -223,7 +230,7 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
||||
:photos="editBatchPhotos"
|
||||
:copilots="flight.copilots"
|
||||
:aircrafts="aircrafts"
|
||||
@save="handleBatchPhotoEdit(flightId, $event)"
|
||||
@save="handleBatchPhotoEdit"
|
||||
@delete="deletePhoto"
|
||||
/>
|
||||
|
||||
@@ -233,5 +240,14 @@ const handleBatchPhotoEdit = async (flightId, 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>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
|
||||
<script setup>
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {onMounted, watch} from "vue";
|
||||
import {onMounted} from "vue";
|
||||
import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
|
||||
import {flightName} from "src/composables/flight";
|
||||
import {useAppStore} from "stores/App";
|
||||
@@ -30,7 +30,7 @@ import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
||||
import {useAirspacesStore} from "stores/Airspaces";
|
||||
import {useAirportsStore} from "stores/Airports";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||
|
||||
const flightId = parseInt(useRoute().params.id);
|
||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||
@@ -50,16 +50,22 @@ const handleDeleteAndRedirect = () => {
|
||||
.onOk(() => {
|
||||
useFlightsStore().destroy(flightId).then(() => {
|
||||
$router.push({name: "myFlights"});
|
||||
$q.notify(successObject(`Úspěšně smazáno`));
|
||||
$q.notify(successObject('Úspěšně smazáno'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const {airspaces} = storeToRefs(useAirspacesStore());
|
||||
const {airports} = storeToRefs(useAirportsStore());
|
||||
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||
|
||||
onMounted(() => {
|
||||
useAirspacesStore().fetch();
|
||||
useAirportsStore().fetch();
|
||||
|
||||
if (flight.value.flightPlanId) {
|
||||
useFlightPlansStore().fetchDetail(flight.value.flightPlanId);
|
||||
}
|
||||
});
|
||||
|
||||
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||
@@ -143,7 +149,7 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
||||
label="Upravit trasu"
|
||||
/>
|
||||
<BasicInfoBanner
|
||||
v-if="flight.track.length === 0 && !flight.gpxTrack"
|
||||
v-if="flight.turnPoints.length === 0 && !flight.track"
|
||||
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
||||
>
|
||||
Nemáš nastavenou žádnou trasu.
|
||||
@@ -156,6 +162,7 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
||||
:show-info-window="true"
|
||||
:show-layer-controls="true"
|
||||
:flight="flight"
|
||||
:flight-plan="flightPlan"
|
||||
:photos="flight.photos"
|
||||
/>
|
||||
</q-no-ssr>
|
||||
|
||||
@@ -14,28 +14,28 @@ import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useAirspacesStore} from "stores/Airspaces";
|
||||
import {useAirportsStore} from "stores/Airports";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
|
||||
import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue";
|
||||
import FlightPlanSideControls from "components/flightPlans/FlightPlanSideControls.vue";
|
||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {successObject} from "src/composables/dialog";
|
||||
import {errorObject, successObject} from "src/composables/dialog";
|
||||
import {useQuasar} from "quasar";
|
||||
import {useCopilotsStore} from "stores/Copilots";
|
||||
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";
|
||||
import FlightPlanVerticalProfile from "components/flightPlans/FlightPlanVerticalProfile.vue";
|
||||
|
||||
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||
|
||||
const flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán");
|
||||
useAppStore().setPageTitle(`${flightPlanName.value} - plánovnání`);
|
||||
useAppStore().setBreadcrumbs([
|
||||
{to: {name: "flightPlans"}, label: "Moje plány"},
|
||||
{label: flightPlanName.value},
|
||||
{label: "Editace"}
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -51,20 +51,9 @@ const {airports} = storeToRefs(useAirportsStore());
|
||||
const {aircrafts: aircraftsData} = storeToRefs(useAircraftsStore());
|
||||
const aircrafts = computed(() => aircraftsData.value.map(a => ({id: a.id, name: a.callSign})));
|
||||
|
||||
|
||||
const {getMarkersFrom} = useFlightPlanUtils();
|
||||
const {markerObject} = useFlightPlanObjects();
|
||||
|
||||
const formData = ref({
|
||||
name: flightPlan.value?.name || null,
|
||||
isDefaultName: flightPlan.value ? flightPlan.value.isDefaultName : true,
|
||||
isEditableByCopilot: flightPlan.value?.isEditableByCopilot || false,
|
||||
isPublic: flightPlan.value?.isPublic || false,
|
||||
plannedTakeoffDatetime: flightPlan.value?.plannedTakeoffDatetime || null,
|
||||
aircraft: flightPlan.value && flightPlan.value.aircraft ? {id: flightPlan.value.aircraft.id, name: flightPlan.value.aircraft.callSign} : null,
|
||||
plannedSpeed: flightPlan.value?.plannedSpeed || 150,
|
||||
})
|
||||
|
||||
const openedTab = ref("route");
|
||||
const flightPlannerDialogOpen = ref(false);
|
||||
const flightPlanMarkers = ref(getMarkersFrom(flightPlan.value));
|
||||
@@ -73,10 +62,11 @@ const handleMarkersChanges = (markers) => {
|
||||
flightPlanMarkers.value = markers;
|
||||
|
||||
useFlightPlansStore().update(flightPlan.value.id, {
|
||||
name: formData.value.name,
|
||||
markers: markers.map(m => markerObject({
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
airportId: m.airportId,
|
||||
pointOfInterestId: m.pointOfInterestId,
|
||||
gpsLatitude: m.gpsLatitude,
|
||||
gpsLongitude: m.gpsLongitude,
|
||||
}))
|
||||
@@ -85,13 +75,74 @@ const handleMarkersChanges = (markers) => {
|
||||
|
||||
|
||||
const $q = useQuasar();
|
||||
const handleFlightPlanInfoSave = () => {
|
||||
useFlightPlansStore().update(flightPlan.value.id, formData.value).then(() => {
|
||||
$q.notify(successObject("Ulozeno"));
|
||||
const handleFlightPlanInfoSave = (formData) => {
|
||||
useFlightPlansStore().update(flightPlan.value.id, formData).then(() => {
|
||||
$q.notify(successObject("Informace úspěšně uloženy"));
|
||||
})
|
||||
}
|
||||
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>
|
||||
@@ -105,13 +156,30 @@ const fullscreen = ref(false);
|
||||
<q-icon name="info"/>
|
||||
Informace
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
|
||||
<q-tab name="navLog">
|
||||
<q-icon name="navigation"/>
|
||||
Navigační štítek
|
||||
</q-tab>
|
||||
|
||||
|
||||
<q-tab name="verticalProfile">
|
||||
<q-icon name="navigation"/>
|
||||
Výškový profil
|
||||
</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">
|
||||
<div :class="{row: true, fullscreen}">
|
||||
<div class="col" style="min-width: 1px">
|
||||
<div class="col map--wrapper" style="min-width: 1px">
|
||||
<FlightPlanMap
|
||||
class="map"
|
||||
:insert-position="insertAtPosition"
|
||||
:airspace-data="airspacesData"
|
||||
:airports="airports"
|
||||
@@ -137,33 +205,43 @@ const fullscreen = ref(false);
|
||||
<q-tab-panel name="info">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
||||
<FormWrapper v-model="formData" @save="handleFlightPlanInfoSave">
|
||||
<q-input v-model="formData.name" label="Jméno letu" v-if="!formData.isDefaultName" v-bind="commonProps"/>
|
||||
|
||||
<q-checkbox v-model="formData.isDefaultName" label="Použít automatické jméno"/>
|
||||
|
||||
<!-- <q-checkbox v-model="formData.isPublic" label="Zveřejnit"/>-->
|
||||
<!-- <q-checkbox v-model="formData.isEditableByCopilot" label="Kopilot může upravovat"/>-->
|
||||
<DatetimePicker v-model="formData.plannedTakeoffDatetime" label="Plánovaný datum a čas vzletu" v-bind="commonProps"/>
|
||||
<ComboboxInput v-model="formData.aircraft" :options="aircrafts" label="Letadlo" v-bind="commonProps"/>
|
||||
|
||||
<q-input v-model.number="formData.plannedSpeed" type="number" step="5" label="Plánovaná rychlost" v-bind="commonProps">
|
||||
<template #append>km/h</template>
|
||||
</q-input>
|
||||
|
||||
<!-- <q-btn @click="handleFlightPlanInfoSave" icon="save">Ulozit</q-btn>-->
|
||||
</FormWrapper>
|
||||
<FlightPlanForm
|
||||
:aircrafts="aircrafts"
|
||||
:flight-plan="flightPlan"
|
||||
@save="handleFlightPlanInfoSave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<q-tab-panel name="navLog">
|
||||
<FlightPlanRouteTable
|
||||
:flight-plan="flightPlan"
|
||||
:planned-speed="flightPlan.plannedSpeed"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="verticalProfile">
|
||||
<FlightPlanVerticalProfile :flight-plan="flightPlan" />
|
||||
</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">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Přidat otočný bod</div>
|
||||
<q-space/>
|
||||
<q-btn icon="close" flat round dense v-close-popup/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
@@ -176,21 +254,29 @@ const fullscreen = ref(false);
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Zavřít" v-close-popup/>
|
||||
<q-btn flat label="Hotovo" v-close-popup/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
z-index: 2001;
|
||||
|
||||
.map--wrapper {
|
||||
height: 100%;
|
||||
|
||||
.map {
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.markers-control {
|
||||
background: white;
|
||||
|
||||
@@ -11,15 +11,13 @@ import FlightPlansList from "components/flightPlans/FlightPlansList.vue";
|
||||
useAppStore().setPageTitle("Nový plán");
|
||||
useAppStore().setBreadcrumbs([
|
||||
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||
{to: {name: "flightPlans"}, label: "Plány"},
|
||||
{label: "Nový"}
|
||||
{label: "Plánované lety"},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
useFlightPlansStore().fetch();
|
||||
});
|
||||
|
||||
|
||||
const {flightPlans, isQueryRunning} = storeToRefs(useFlightPlansStore());
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const $router = useRouter();
|
||||
onMounted(async () => {
|
||||
await useFlightPlansStore().create({
|
||||
name: "",
|
||||
description: "",
|
||||
isDefaultName: true,
|
||||
isPublic: false,
|
||||
isEditableByCopilot: false,
|
||||
|
||||
@@ -31,9 +31,9 @@ useMeta(() => metaData({
|
||||
Pro obohacení informací používá Polétání.cz data z následujících zdrojů:
|
||||
|
||||
<ul>
|
||||
<li>Open Meteo API - pro stahování informací o počasí při vzletu a přistání</li>
|
||||
<li>Open Meteo API - pro stahování informací o počasí při vzletu a přistání, pro stahování informací o elevaci terénu</li>
|
||||
<li>Elevation API - pro stažení informací o výšce terénu. Tato informace se používá u fotografií a trasy letu</li>
|
||||
<li>VFR příručka - pro aktualizaci informací o letištích</li>
|
||||
<li>OpenAIP - primární zdroj dat o vzdušném prostoru a letištích</li>
|
||||
</ul>
|
||||
|
||||
<h3>O autorovi</h3>
|
||||
|
||||
@@ -25,7 +25,7 @@ const sendMessage = () => {
|
||||
email_from: formData.value.email,
|
||||
content: formData.value.content
|
||||
}).then((e) => {
|
||||
$q.notify(successObject("Zpráva byla jakoby odeslána"));
|
||||
$q.notify(successObject("Zpráva byla odeslána"));
|
||||
formData.value = {email: "", content: ""};
|
||||
}).catch(() => {
|
||||
$q.notify(errorObject("Zprávu se nepodařilo odeslat. Zkus to znovu za chvíli."));
|
||||
|
||||
@@ -18,6 +18,7 @@ useAppStore().setBreadcrumbs([
|
||||
Sdílet zážitky s kamarády
|
||||
Upravovat fotky
|
||||
Zobrazovat je v kontextu polohy
|
||||
Planovat let
|
||||
Debriefing letu: https://www.easa.europa.eu/community/topics/post-flight-debrief
|
||||
|
||||
</q-card-section>
|
||||
|
||||
@@ -39,7 +39,7 @@ const {flights, totalItemsCount, isQueryRunning: areFlightsLoading} = storeToRef
|
||||
const {photos, isQueryRunning: arePhotosLoading} = storeToRefs(usePhotosStore());
|
||||
|
||||
useAppStore().setPageTitle(`${props.pilot.name} + ${copilot.value.name}`);
|
||||
useAppStore().setBreadcrumbs(publicCopilotBreadcrumbs(props.pilot));
|
||||
useAppStore().setBreadcrumbs(publicCopilotBreadcrumbs(props.pilot, null, null, copilot.value));
|
||||
|
||||
useMeta(() => metaData({
|
||||
title: copilot.value.name,
|
||||
|
||||
@@ -22,8 +22,11 @@ import {useMeta} from "quasar";
|
||||
import {aircraftKeywords, flightEventKeywords, flightKeywords, metaData} from "src/composables/meta";
|
||||
import ContentCard from "components/ContentCard.vue";
|
||||
import {usePhotosStore} from "stores/Photos";
|
||||
import {computed} from "vue";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {publicFlightBreadcrumbs} from "src/composables/breadcrumbs";
|
||||
import {useAirspacesStore} from "stores/Airspaces";
|
||||
import {useAirportsStore} from "stores/Airports";
|
||||
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||
|
||||
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
|
||||
|
||||
@@ -34,8 +37,19 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
usePhotosStore().fetchPublic({flightId: flight.value.id});
|
||||
useAirspacesStore().fetch();
|
||||
useAirportsStore().fetch();
|
||||
|
||||
if (flight.value.flightPlanId) {
|
||||
useFlightPlansStore().fetchPublicDetail({username: props.pilot.publicUsername, id: flight.value.flightPlanId});
|
||||
}
|
||||
})
|
||||
const {isQueryRunning: arePhotosLoading, photos: flightPhotos} = storeToRefs(usePhotosStore());
|
||||
const {airspaces} = storeToRefs(useAirspacesStore());
|
||||
const {airports} = storeToRefs(useAirportsStore());
|
||||
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||
|
||||
const flightNameFormatted = flightName(flight.value);
|
||||
const event = computed(() => flight.value.event);
|
||||
@@ -52,7 +66,7 @@ useMeta(() => metaData({
|
||||
...flightEventKeywords(event.value)
|
||||
],
|
||||
description: flight.value.description,
|
||||
image: flight.value.titlePhoto?.url
|
||||
image: flight.value.socialImageUrl
|
||||
}
|
||||
));
|
||||
</script>
|
||||
@@ -78,8 +92,12 @@ useMeta(() => metaData({
|
||||
<q-inner-loading v-if="arePhotosLoading"/>
|
||||
<PhotoGallery :photos="flightPhotos" v-else-if="!arePhotosLoading && flightPhotos.length > 0"/>
|
||||
<FlightTrackMap
|
||||
:airspaces-data="airspaces"
|
||||
:airports="airports"
|
||||
:key="flight.id"
|
||||
:flight="flight"
|
||||
:flight-plan="flightPlan"
|
||||
:show-info-window="true"
|
||||
:showLayerControls="false"
|
||||
:photos="flightPhotos"/>
|
||||
</q-no-ssr>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script>
|
||||
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||
|
||||
export default {
|
||||
preFetch({store, currentRoute}) {
|
||||
return useFlightPlansStore(store).fetchPublicDetail({
|
||||
id: currentRoute.params.id,
|
||||
username: currentRoute.params.username
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script setup>
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {publicProfileBreadcrumbs} from "src/composables/breadcrumbs";
|
||||
import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {useAirspacesStore} from "stores/Airspaces";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {useAirportsStore} from "stores/Airports";
|
||||
import {formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {useFlightPlanCalculations, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||
import FlightPlanRouteTable from "components/flightPlans/FlightPlanRouteTable.vue";
|
||||
import {useMeta} from "quasar";
|
||||
import {metaData} from "src/composables/meta";
|
||||
|
||||
const props = defineProps({
|
||||
pilot: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||
|
||||
useAppStore().setPageTitle(flightPlan.value.name);
|
||||
useMeta(() => metaData({
|
||||
title: `Plán: ${flightPlan.value.name}`,
|
||||
keywords: [flightPlan.value.name, "plán", "trasa", "navigace", "štítek", "mapa", flightPlan.value.aircraft?.callSign],
|
||||
description: flightPlan.value.description,
|
||||
}));
|
||||
useAppStore().setBreadcrumbs([
|
||||
...publicProfileBreadcrumbs(props.pilot),
|
||||
{label: `Plán: ${flightPlan.value.name}`},
|
||||
]);
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
useAirspacesStore().fetch();
|
||||
usePointsOfInterestStore().fetchPublic({limit: 10000});
|
||||
useAirportsStore().fetch();
|
||||
});
|
||||
|
||||
const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||
const {airspaces: airspacesData, areAirspacesLoading: isQueryRunning} = storeToRefs(useAirspacesStore())
|
||||
const {airports} = storeToRefs(useAirportsStore());
|
||||
|
||||
const {getMarkersFrom} = useFlightPlanUtils();
|
||||
|
||||
const usedPointsOfInterest = computed(() => {
|
||||
const poisInTrackIds = getMarkersFrom(flightPlan.value).filter(m => !!m.pointOfInterestId).map(poi => poi.pointOfInterestId);
|
||||
return pointsOfInterest.value.filter(poi => poisInTrackIds.includes(poi.id))
|
||||
});
|
||||
|
||||
const openedTab = ref("map");
|
||||
|
||||
const {getTotalDistance, getTotalDuration} = useFlightPlanCalculations();
|
||||
|
||||
const showInfoCard = computed(() => flightPlan.value.aircraft || flightPlan.value.plannedTakeoffDatetime || flight.value.description);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-tabs v-model="openedTab">
|
||||
<q-tab name="map">Mapa</q-tab>
|
||||
<q-tab name="trackTable">Trasa</q-tab>
|
||||
<q-tab name="info" v-if="showInfoCard">Informace</q-tab>
|
||||
</q-tabs>
|
||||
|
||||
<q-tab-panels v-model="openedTab" class="q-mb-lg">
|
||||
<q-tab-panel name="map">
|
||||
<FlightPlanMap
|
||||
:airspace-data="airspacesData"
|
||||
:airports="airports"
|
||||
:points-of-interest="usedPointsOfInterest"
|
||||
:flight-plan="flightPlan"
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-chip size="lg">
|
||||
<q-icon class="q-mx-md" name="mdi-map-marker-distance"/>
|
||||
{{ getTotalDistance(flightPlan) }} km
|
||||
</q-chip>
|
||||
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<q-chip size="lg">
|
||||
<q-icon name="mdi-clock-outline"/>
|
||||
{{ minutesToHumanReadable(getTotalDuration(flightPlan, flightPlan.plannedSpeed)) }}
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="trackTable">
|
||||
<FlightPlanRouteTable :planned-speed="flightPlan.plannedSpeed" :flight-plan="flightPlan"/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="info" v-if="showInfoCard">
|
||||
<div v-if="flightPlan.plannedTakeoffDatetime">
|
||||
Plánovaný vzlet: {{ formatDatetime(flightPlan.plannedTakeoffDatetime) }}
|
||||
<br>
|
||||
</div>
|
||||
<div v-if="flightPlan.aircraft">
|
||||
<q-avatar v-if="flightPlan.aircraft.titlePhoto" class="q-ma-md">
|
||||
<img :src="flightPlan.aircraft.titlePhoto.thumbnailUrl" alt=""/>
|
||||
</q-avatar>
|
||||
<router-link v-if="flightPlan.aircraft.isPublic" :to="{name: 'publicAircraftDetail', params: {callSign: flightPlan.aircraft.callSign}}">
|
||||
{{ flightPlan.aircraft.callSign }}
|
||||
</router-link>
|
||||
<template v-else>
|
||||
{{ flightPlan.aircraft.callSign }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-html="flightPlan.description"/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</template>
|
||||
@@ -22,7 +22,7 @@ useMeta(() => metaData({
|
||||
title: `${props.pilot.publicUsername} - Lety`,
|
||||
keywords: [...commonKeywords, "pilot", props.pilot.publicUsername, props.pilot.name],
|
||||
description: `Výlety publikované pilotem ${props.pilot.name}. ${props.pilot.publicUsername}: ${props.pilot.description}`,
|
||||
image: props.pilot.titleImageUrl
|
||||
image: props.pilot.socialImageUrl
|
||||
}));
|
||||
|
||||
useAppStore().setPageTitle(props.pilot.publicUsername);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {useMeta} from "quasar";
|
||||
import {metaData} from "src/composables/meta";
|
||||
import {publicPOIBaseBreadcrumbs} from "src/composables/breadcrumbs";
|
||||
import {onMounted} from "vue";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import {useGeocode} from "src/composables/geocoding";
|
||||
|
||||
useAppStore().setPageTitle("Místa");
|
||||
useAppStore().setBreadcrumbs(publicPOIBaseBreadcrumbs());
|
||||
useMeta(() => metaData({
|
||||
title: "Místa"
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
usePointsOfInterestStore().fetchPublic({limit: 10000})
|
||||
});
|
||||
const {center} = useGeocode();
|
||||
const {pointsOfInterest, isQueryRunning} = storeToRefs(usePointsOfInterestStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-skeleton v-if="isQueryRunning" type="rect" style="width: 100%; height: 65vh; min-height: 100px" />
|
||||
<MapWrapper
|
||||
v-show="!isQueryRunning"
|
||||
style="width: 100%; height: 65vh; min-height: 100px"
|
||||
:center="center"
|
||||
:show-layer-controls="false"
|
||||
:show-info-window="true"
|
||||
:zoom="8"
|
||||
:points-of-interest="pointsOfInterest"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,7 +8,8 @@ import {useMeta} from "quasar";
|
||||
import {metaData} from "src/composables/meta";
|
||||
import {usePagination} from "src/composables/pagination";
|
||||
import ListFilters from "components/ListFilters.vue";
|
||||
import {homepageBreadcrumbs, publicPOIBaseBreadcrumbs} from "src/composables/breadcrumbs";
|
||||
import {publicPOIBaseBreadcrumbs} from "src/composables/breadcrumbs";
|
||||
import {onMounted} from "vue";
|
||||
|
||||
useAppStore().setPageTitle("Místa");
|
||||
useAppStore().setBreadcrumbs(publicPOIBaseBreadcrumbs());
|
||||
@@ -19,7 +20,10 @@ useMeta(() => metaData({
|
||||
const {filters, loadPage, paginationOffset, paginationLimit} = usePagination({
|
||||
fetchFunction: usePointsOfInterestStore().fetchPublic
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadPage(false);
|
||||
});
|
||||
|
||||
const handleFiltersChanged = (newFilters) => {
|
||||
paginationOffset.value = 0;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-tabs>
|
||||
<q-route-tab :to="{name: 'publicPOIsList'}">Seznam</q-route-tab>
|
||||
<q-route-tab :to="{name: 'publicPOIsMap'}">Mapa</q-route-tab>
|
||||
</q-tabs>
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
+2
-2
@@ -3,8 +3,8 @@ import {createRouter, createMemoryHistory, createWebHistory} from 'vue-router'
|
||||
import routes from './routes'
|
||||
import {useAppStore} from "stores/App";
|
||||
|
||||
export default route(function ({ store }) {
|
||||
const createHistory = process.env.SERVER ? createMemoryHistory : createWebHistory;
|
||||
export default route(function ({ store, ssrContext }) {
|
||||
const createHistory = ssrContext ? createMemoryHistory : createWebHistory;
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({left: 0, top: 0}),
|
||||
|
||||
@@ -18,7 +18,14 @@ const routes = [
|
||||
{path: 'osobni-udaje', name: "personalInformation", component: () => import('pages/public/PersonalInformationPage.vue')},
|
||||
{path: 'funkce', name: "features", component: () => import('pages/public/FeaturesPage.vue')},
|
||||
{path: 'letadlo/:callSign', name: "publicAircraftDetail", component: () => import('pages/public/aircraft/AircraftDetailPage.vue')},
|
||||
{path: 'mista', name: "publicPOIs", component: () => import('pages/public/poi/PointsOfInterestPage.vue')},
|
||||
{
|
||||
path: 'mista',
|
||||
component: () => import('pages/public/poi/PointsOfInterestWrapper.vue'),
|
||||
children: [
|
||||
{path: '', name: "publicPOIsList", component: () => import('pages/public/poi/PointsOfInterestPage.vue')},
|
||||
{path: 'mapa', name: "publicPOIsMap", component: () => import('pages/public/poi/PointsOfInterestMapPage.vue')},
|
||||
]
|
||||
},
|
||||
{path: 'mista/:slug', name: "publicPOIDetail", component: () => import('pages/public/poi/PointOfInterestDetailPage.vue')},
|
||||
{path: 'napoveda', name: "help", component: () => import('pages/public/HelpPage.vue')},
|
||||
{
|
||||
@@ -31,6 +38,7 @@ const routes = [
|
||||
{path: 'a/:copilotSlug', name: "publicCopilotDetail", component: () => import('pages/public/pilotProfile/CopilotDetailPage.vue')},
|
||||
{path: 'udalosti', name: "publicEventsList", component: () => import('pages/public/pilotProfile/EventsListPage.vue')},
|
||||
{path: 'udalosti/:eventSlug', name: "publicEventDetail", component: () => import('pages/public/pilotProfile/EventDetailPage.vue')},
|
||||
{path: 'plan/:id', name: "publicFlightPlanDetail", component: () => import("pages/public/pilotProfile/FlightPlanDetail.vue")}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -18,6 +18,14 @@ export const useFlightPlansStore = defineStore('FlightPlans', () => {
|
||||
}).then(item => flightPlan.value = item);
|
||||
};
|
||||
|
||||
const fetchPublicDetail = ({username, id}) => {
|
||||
return query({
|
||||
isQueryRunning,
|
||||
query: FLIGHT_PLAN_DETAIL,
|
||||
variables: {username, id, public: true},
|
||||
}).then(item => flightPlan.value = item);
|
||||
}
|
||||
|
||||
const fetch = () => {
|
||||
return query({
|
||||
isQueryRunning,
|
||||
@@ -50,6 +58,7 @@ export const useFlightPlansStore = defineStore('FlightPlans', () => {
|
||||
|
||||
return {
|
||||
flightPlan, flightPlans, isMutationRunning, isQueryRunning,
|
||||
fetch, fetchDetail, create, update
|
||||
fetch, fetchDetail, create, update,
|
||||
fetchPublicDetail
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user