423 lines
13 KiB
Vue
423 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeMount, ref } from 'vue';
|
|
import { useQuasar } from 'quasar';
|
|
|
|
import SetLocationDialog from 'components/SetLocationDialog.vue';
|
|
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
|
|
import { useAddLocation } from 'composables/useAddLocation';
|
|
import { actionCircle } from 'components/leaflet/actionCircle';
|
|
|
|
import { useLeafletStore } from 'stores/leaflet';
|
|
import { useLocationStore } from 'stores/location';
|
|
import { useFavoriteStore } from 'stores/favorite';
|
|
import { storeToRefs } from 'pinia';
|
|
|
|
import * as L from 'leaflet';
|
|
import { LPopup } from '@vue-leaflet/vue-leaflet';
|
|
import { Icon, PinStar } from 'leaflet-extra-markers';
|
|
|
|
import { startIcon, endIcon, iconElement } from 'constants/icons';
|
|
import type { LatLngExpression } from 'leaflet';
|
|
|
|
import type { Favorite, FavoriteCommands, GeoCacheResponse } from 'components/models';
|
|
import FormattedAddress from 'components/FormattedAddress.vue';
|
|
|
|
const props = defineProps({
|
|
latLng: {
|
|
type: Object as () => LatLngExpression,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const isFavorite = ref(false);
|
|
const favorite = ref();
|
|
const loading = ref(true);
|
|
|
|
const $q = useQuasar();
|
|
|
|
const favoriteStore = useFavoriteStore();
|
|
const locationStore = useLocationStore();
|
|
|
|
const leafletStore = useLeafletStore();
|
|
const { clickedLatLng, routeSet, layerControl, layerGroups } = storeToRefs(leafletStore);
|
|
|
|
type AddLocationDialogPayload = {
|
|
delay: number | undefined;
|
|
address?: string;
|
|
};
|
|
|
|
async function addDialogLocation(latlng: L.LatLng, { delay, address }: AddLocationDialogPayload) {
|
|
const revGeo = !address;
|
|
const { notificationType, responseMessage } = await useAddLocation(
|
|
{ lat: Number(latlng.lat), lng: Number(latlng.lng) },
|
|
revGeo,
|
|
delay,
|
|
address,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
|
|
$q.notify({ type: notificationType, message: responseMessage });
|
|
|
|
console.log(
|
|
'Confirmed location add: latitude: ' +
|
|
latlng.lat +
|
|
', longitude: ' +
|
|
latlng.lng +
|
|
', delay: ' +
|
|
delay,
|
|
);
|
|
}
|
|
|
|
function handleAddLocation() {
|
|
if (props.latLng) {
|
|
const latlng = props.latLng;
|
|
closeAllPopups();
|
|
$q.dialog({
|
|
component: SetLocationDialog,
|
|
componentProps: {
|
|
lat: Number(L.latLng(latlng).lat),
|
|
lng: Number(L.latLng(latlng).lng),
|
|
},
|
|
})
|
|
.onOk((payload: AddLocationDialogPayload) => {
|
|
void addDialogLocation(L.latLng(latlng), payload);
|
|
})
|
|
.onCancel(() => {
|
|
console.log('Dialog cancelled');
|
|
})
|
|
.onDismiss(() => {
|
|
console.log('Dialog dismissed');
|
|
});
|
|
}
|
|
}
|
|
|
|
async function addFavorite() {
|
|
console.log('Adding favorite for', props.latLng);
|
|
if (props.latLng) {
|
|
const defaultRadius = 50;
|
|
const latlng = props.latLng;
|
|
layerControl.value.favorites = true;
|
|
layerControl.value.marker = false;
|
|
let geoCacheResp: GeoCacheResponse | undefined = await locationStore.getCachedLocation(latlng);
|
|
if (geoCacheResp) {
|
|
console.log('Using cached location for favorite:', geoCacheResp);
|
|
} else {
|
|
console.log('No cached location found, creating new favorite');
|
|
}
|
|
if (layerGroups.value.favorites) {
|
|
closeAllPopups();
|
|
const favoriteIcon = new Icon({
|
|
color: '#f5bb39',
|
|
accentColor: 'rbga(0,0,0,0.25)',
|
|
contentHtml: '<icon class="q-icon mdi mdi-star" />',
|
|
contentColor: 'black',
|
|
scale: 1,
|
|
svg: PinStar,
|
|
});
|
|
const favoriteMarker = L.marker(L.latLng(latlng), {
|
|
icon: favoriteIcon,
|
|
draggable: true,
|
|
contextmenu: false,
|
|
contextmenuItems: [],
|
|
});
|
|
const circle = actionCircle(L.latLng(latlng), {
|
|
radius: defaultRadius,
|
|
color: 'black',
|
|
fillColor: '#f5bb39',
|
|
fillOpacity: 0.25,
|
|
onAccept: (instance) => {
|
|
console.log(
|
|
'Confirm favorite coordinates: ' +
|
|
instance.getLatLng().toString() +
|
|
', radius: ' +
|
|
instance.getRadius() +
|
|
', geoCacheResp: ',
|
|
);
|
|
$q.notify(`Adding favorite...${instance.getLatLng().toString()}`);
|
|
$q.dialog({
|
|
component: EditFavoriteDialog,
|
|
componentProps: {
|
|
lat: Number(instance.getLatLng().lat),
|
|
lng: Number(instance.getLatLng().lng),
|
|
radius: instance.getRadius(),
|
|
operation: 'set',
|
|
geoCacheResp: geoCacheResp,
|
|
},
|
|
})
|
|
.onOk((response: { operation: FavoriteCommands; data: Favorite }) => {
|
|
void favoriteStore.favoriteControl({
|
|
command: response.operation,
|
|
favorite: response.data,
|
|
});
|
|
newFavGrp.clearLayers();
|
|
layerControl.value.marker = true;
|
|
})
|
|
.onCancel(() => {
|
|
console.log('Dialog cancelled');
|
|
})
|
|
.onDismiss(() => {
|
|
console.log('Dialog dismissed');
|
|
});
|
|
},
|
|
onClear: () => {
|
|
console.log('Clear favorite radius');
|
|
newFavGrp.clearLayers();
|
|
layerControl.value.marker = true;
|
|
},
|
|
});
|
|
const handleLatLng = (latlng: L.LatLngExpression, dist: number = 50): L.LatLngExpression => {
|
|
const lat = L.latLng(latlng).lat;
|
|
const lng = L.latLng(latlng).lng;
|
|
const brearing = 270;
|
|
const brng = (brearing * Math.PI) / 180;
|
|
const R = 6371e3;
|
|
const lat1 = (lat * Math.PI) / 180;
|
|
const lon1 = (lng * Math.PI) / 180;
|
|
const lat2 = Math.asin(
|
|
Math.sin(lat1) * Math.cos(dist / R) +
|
|
Math.cos(lat1) * Math.sin(dist / R) * Math.cos(brng),
|
|
);
|
|
const lon2 =
|
|
lon1 +
|
|
Math.atan2(
|
|
Math.sin(brng) * Math.sin(dist / R) * Math.cos(lat1),
|
|
Math.cos(dist / R) - Math.sin(lat1) * Math.sin(lat2),
|
|
);
|
|
return [(lat2 * 180) / Math.PI, (lon2 * 180) / Math.PI];
|
|
};
|
|
const handleIcon = L.divIcon({
|
|
html: '<i class="q-icon mdi mdi-hand-pointing-right text-black" style="font-size: 32px;" />',
|
|
className: 'custom-marker-with-buttons',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [28, 16],
|
|
});
|
|
const handleMarker = L.marker(handleLatLng(latlng), {
|
|
draggable: true,
|
|
icon: handleIcon,
|
|
contextmenu: false,
|
|
contextmenuItems: [],
|
|
});
|
|
const newFavGrp: L.FeatureGroup = L.featureGroup([favoriteMarker, circle, handleMarker]);
|
|
leafletStore.addToFavLayer(newFavGrp);
|
|
let startPosition: L.LatLng;
|
|
handleMarker.on('dragstart', function (e) {
|
|
startPosition = e.target.getLatLng();
|
|
});
|
|
handleMarker.on('drag', function (e) {
|
|
const newLatLng = e.target.getLatLng();
|
|
const circleLatLng = circle.getLatLng();
|
|
let lng: number;
|
|
if (newLatLng.lng > circleLatLng.lng) {
|
|
lng = circleLatLng.lng;
|
|
} else {
|
|
lng = newLatLng.lng;
|
|
}
|
|
const forcedLatLng = L.latLng({ lat: startPosition.lat, lng: lng });
|
|
handleMarker.setLatLng(forcedLatLng);
|
|
const newRadius = leafletStore.getDistance(circleLatLng, forcedLatLng);
|
|
if (newRadius) {
|
|
circle.setRadius(newRadius);
|
|
}
|
|
});
|
|
favoriteMarker.on('drag', function (e) {
|
|
const newLatLng = e.target.getLatLng();
|
|
circle.setLatLng(newLatLng);
|
|
handleMarker.setLatLng(handleLatLng(newLatLng, circle.getRadius()));
|
|
});
|
|
favoriteMarker.on('dragend', function (e) {
|
|
const newLatLng = e.target.getLatLng();
|
|
void (async () => {
|
|
try {
|
|
geoCacheResp = await locationStore.getCachedLocation(newLatLng);
|
|
console.log('Marker dragend');
|
|
} catch (error) {
|
|
console.error('Error handling marker dragend:', error);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
}
|
|
leafletStore.zoom = 20;
|
|
}
|
|
/*
|
|
function clearFavorite() {
|
|
closeAllPopups();
|
|
if (!props.latLng) return;
|
|
const latlng = L.latLng(props.latLng);
|
|
let geoCacheResp: GeoCacheResponse | undefined;
|
|
void (async () => {
|
|
try {
|
|
geoCacheResp = await locationStore.getCachedLocation(latlng);
|
|
} catch (error) {
|
|
console.error('Error finding cached location:', error);
|
|
}
|
|
if (geoCacheResp?.favorite) {
|
|
$q.notify(`Clearing favorite...${geoCacheResp.favorite.name?.toString()}`);
|
|
$q.dialog({
|
|
component: EditFavoriteDialog,
|
|
componentProps: {
|
|
lat: Number(latlng.lat),
|
|
lng: Number(latlng.lng),
|
|
radius: geoCacheResp.favorite.radius,
|
|
operation: 'clear',
|
|
geoCacheResp: geoCacheResp,
|
|
},
|
|
})
|
|
.onOk((response: { operation: FavoriteCommands; data: Favorite }) => {
|
|
void favoriteStore.favoriteControl({
|
|
command: response.operation,
|
|
favorite: response.data,
|
|
});
|
|
})
|
|
.onCancel(() => {
|
|
console.log('Dialog cancelled');
|
|
})
|
|
.onDismiss(() => {
|
|
console.log('Dialog dismissed');
|
|
});
|
|
}
|
|
})();
|
|
}
|
|
*/
|
|
|
|
function closeAllPopups() {
|
|
leafletStore.closeAllPopups();
|
|
}
|
|
|
|
const setStartRoute = () => {
|
|
leafletStore.layerControl.routing = true;
|
|
if (!clickedLatLng.value) return;
|
|
closeAllPopups();
|
|
leafletStore.routeSet.start = clickedLatLng.value;
|
|
console.log('setStartRoute: ', routeSet.value.start);
|
|
};
|
|
|
|
const setEndRoute = () => {
|
|
leafletStore.layerControl.routing = true;
|
|
if (!clickedLatLng.value) return;
|
|
leafletStore.routeSet.end = clickedLatLng.value;
|
|
closeAllPopups();
|
|
console.log('setEndRoute: ', leafletStore.routeSet.end);
|
|
leafletStore.updateRouteWaypoints();
|
|
console.log('updating Route');
|
|
};
|
|
|
|
onBeforeMount(async () => {
|
|
try {
|
|
console.log('Fetching geoCacheResp...');
|
|
const geoCacheResp = await locationStore.getCachedLocation(props.latLng);
|
|
console.log('geoCacheResp: ', geoCacheResp);
|
|
if (geoCacheResp?.favorite) {
|
|
favorite.value = geoCacheResp.favorite;
|
|
}
|
|
isFavorite.value = !!geoCacheResp?.favorite;
|
|
} catch (error) {
|
|
console.error('Error fetching cached location:', error);
|
|
throw error;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<L-Popup :options="{ closeOnClick: true, closeButton: false, className: 'marker-popup' }">
|
|
<q-list dense seperator style="min-width: 100px" class="bg-grey-10">
|
|
<q-item v-if="isFavorite" class="text-center">
|
|
<q-item-section>
|
|
<q-item-label>{{ favorite.name }}</q-item-label>
|
|
<q-item-label caption lines="2" v-if="favorite.address"
|
|
><FormattedAddress :address="favorite.address"></FormattedAddress
|
|
></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-separator v-if="isFavorite" class="q-my-sm" />
|
|
<q-item clickable v-ripple @click="handleAddLocation">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="mdi-map-marker" color="accent" text-color="white" size="sm" />
|
|
</q-item-section>
|
|
<q-item-section>Add Location</q-item-section>
|
|
</q-item>
|
|
<q-item clickable v-ripple @click="setStartRoute">
|
|
<q-item-section avatar>
|
|
<q-avatar color="accent" size="sm">
|
|
<q-icon class="small-icon" v-html="iconElement(startIcon).outerHTML" />
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>Set Route Start</q-item-section>
|
|
</q-item>
|
|
<q-item clickable v-ripple @click="setEndRoute">
|
|
<q-item-section avatar>
|
|
<q-avatar color="accent" size="sm">
|
|
<q-icon class="small-icon" v-html="iconElement(endIcon).outerHTML" />
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>Set Route End</q-item-section>
|
|
</q-item>
|
|
<q-item v-if="loading">
|
|
<q-item-section avatar>
|
|
<q-spinner-hourglass color="accent" size="2em" />
|
|
</q-item-section>
|
|
<q-item-section>Loading...</q-item-section>
|
|
</q-item>
|
|
<q-item clickable v-ripple @click="addFavorite" v-else-if="!isFavorite && !loading">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="mdi-star-outline" color="accent" text-color="yellow" size="sm" />
|
|
</q-item-section>
|
|
<q-item-section>Set Favorite</q-item-section>
|
|
</q-item>
|
|
<q-item clickable v-ripple @click="addFavorite" v-else-if="isFavorite && !loading">
|
|
<q-item-section avatar>
|
|
<q-avatar icon="mdi-star-outline" color="accent" text-color="yellow" size="sm" />
|
|
</q-item-section>
|
|
<q-item-section>Edit Favorite</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</L-Popup>
|
|
</template>
|
|
|
|
<style lang="sass">
|
|
.marker-popup .leaflet-popup-content-wrapper, .marker-popup .leaflet-popup-tip
|
|
background-color: $dark
|
|
|
|
.marker-popup .leaflet-popup-content
|
|
margin: 13px 10px 13px 10px
|
|
|
|
.leaflet-circle-actions
|
|
pointer-events: none
|
|
|
|
.leaflet-circle-actions button
|
|
border: 2px solid rgba(0,0,0,0.2)
|
|
background-clip: padding-box
|
|
cursor: pointer
|
|
text-align: center
|
|
font-weight: bold
|
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4)
|
|
|
|
.circle-btn-accept
|
|
background-color: $positive
|
|
color: black
|
|
.circle-btn-clear
|
|
|
|
background-color: $negative
|
|
color: black
|
|
|
|
.leaflet-circle-actions button:hover
|
|
background-color: #f4f4f4
|
|
|
|
.leaflet-overlay-pane svg
|
|
position: absolute
|
|
|
|
.small-icon svg
|
|
transform: scale(0.6)
|
|
transform-origin: center
|
|
bottom: 0
|
|
right: 0
|
|
|
|
.small-icon
|
|
position: absolute
|
|
margin-left: 14px
|
|
margin-top: 44px
|
|
</style>
|