milestones
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user