2023-01-25 18:41:55 +00:00
|
|
|
<template>
|
2023-02-08 22:13:13 +00:00
|
|
|
<div class="map-matter">
|
2023-02-09 05:55:12 +00:00
|
|
|
<LMap
|
2023-02-08 21:35:42 +00:00
|
|
|
class="map"
|
2023-01-26 04:51:42 +00:00
|
|
|
ref="map"
|
2023-02-08 21:35:42 +00:00
|
|
|
:zoom="zoom"
|
|
|
|
:minZoom="2"
|
|
|
|
@moveend="refresh"
|
|
|
|
@zoomend="refresh"
|
2023-01-26 04:51:42 +00:00
|
|
|
>
|
2023-02-09 05:55:12 +00:00
|
|
|
<LTileLayer :url="url" :attribution="attribution" />
|
|
|
|
<LMarker
|
2023-02-08 03:59:04 +00:00
|
|
|
v-for="cluster in clusters"
|
2023-02-09 07:55:54 +00:00
|
|
|
:key="cluster.id"
|
2023-02-08 03:59:04 +00:00
|
|
|
:lat-lng="cluster.center"
|
2023-02-09 07:15:48 +00:00
|
|
|
@click="zoomTo(cluster.center)"
|
2023-02-08 03:59:04 +00:00
|
|
|
>
|
2023-02-09 07:55:54 +00:00
|
|
|
<LIcon :icon-anchor="[24, 24]">
|
2023-02-09 05:55:12 +00:00
|
|
|
<div class="preview">
|
2023-02-09 09:01:15 +00:00
|
|
|
<div class="count" v-if="cluster.count > 1">
|
|
|
|
{{ cluster.count }}
|
|
|
|
</div>
|
2023-02-09 05:55:12 +00:00
|
|
|
<img :src="clusterPreviewUrl(cluster)" />
|
|
|
|
</div>
|
|
|
|
</LIcon>
|
|
|
|
</LMarker>
|
|
|
|
</LMap>
|
2023-01-26 04:51:42 +00:00
|
|
|
</div>
|
2023-01-25 18:41:55 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
2023-02-08 21:35:42 +00:00
|
|
|
import { defineComponent } from "vue";
|
2023-02-09 05:55:12 +00:00
|
|
|
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet";
|
2023-01-26 04:51:42 +00:00
|
|
|
import { Icon } from "leaflet";
|
2023-02-08 21:35:42 +00:00
|
|
|
|
2023-02-08 03:59:04 +00:00
|
|
|
import { API } from "../../services/API";
|
2023-02-09 16:29:53 +00:00
|
|
|
import axios from "@nextcloud/axios";
|
2023-01-25 18:41:55 +00:00
|
|
|
|
2023-02-08 21:35:42 +00:00
|
|
|
import "leaflet/dist/leaflet.css";
|
2023-01-25 18:41:55 +00:00
|
|
|
|
2023-02-08 21:35:42 +00:00
|
|
|
const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
|
|
const ATTRIBUTION =
|
|
|
|
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
2023-01-25 18:41:55 +00:00
|
|
|
|
2023-02-08 21:53:38 +00:00
|
|
|
type IMarkerCluster = {
|
2023-02-09 05:55:12 +00:00
|
|
|
id?: number;
|
2023-02-09 16:25:37 +00:00
|
|
|
u?: any;
|
2023-02-08 21:53:38 +00:00
|
|
|
center: [number, number];
|
|
|
|
count: number;
|
|
|
|
};
|
|
|
|
|
2023-01-25 18:41:55 +00:00
|
|
|
Icon.Default.mergeOptions({
|
2023-01-26 04:51:42 +00:00
|
|
|
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
|
|
|
|
iconUrl: require("leaflet/dist/images/marker-icon.png"),
|
|
|
|
shadowUrl: require("leaflet/dist/images/marker-shadow.png"),
|
2023-01-25 18:41:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
export default defineComponent({
|
2023-02-08 22:13:13 +00:00
|
|
|
name: "MapSplitMatter",
|
2023-01-26 04:51:42 +00:00
|
|
|
components: {
|
|
|
|
LMap,
|
|
|
|
LTileLayer,
|
|
|
|
LMarker,
|
|
|
|
LPopup,
|
2023-02-09 05:55:12 +00:00
|
|
|
LIcon,
|
2023-01-26 04:51:42 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
data: () => ({
|
2023-02-08 21:35:42 +00:00
|
|
|
url: TILE_URL,
|
|
|
|
attribution: ATTRIBUTION,
|
|
|
|
zoom: 2,
|
|
|
|
clusters: [] as IMarkerCluster[],
|
2023-01-26 04:51:42 +00:00
|
|
|
}),
|
|
|
|
|
|
|
|
mounted() {
|
2023-02-08 21:35:42 +00:00
|
|
|
const map = this.$refs.map as LMap;
|
2023-01-25 18:41:55 +00:00
|
|
|
|
2023-02-08 21:35:42 +00:00
|
|
|
// Make sure the zoom control doesn't overlap with the navbar
|
|
|
|
map.mapObject.zoomControl.setPosition("topright");
|
|
|
|
|
|
|
|
// Initialize
|
|
|
|
this.refresh();
|
2023-01-26 04:51:42 +00:00
|
|
|
},
|
2023-01-25 18:41:55 +00:00
|
|
|
|
2023-01-26 04:51:42 +00:00
|
|
|
methods: {
|
2023-02-08 21:35:42 +00:00
|
|
|
async refresh() {
|
|
|
|
const map = this.$refs.map as LMap;
|
|
|
|
|
|
|
|
// Get boundaries of the map
|
|
|
|
const boundary = map.mapObject.getBounds();
|
|
|
|
const minLat = boundary.getSouth();
|
|
|
|
const maxLat = boundary.getNorth();
|
|
|
|
const minLon = boundary.getWest();
|
|
|
|
const maxLon = boundary.getEast();
|
|
|
|
|
|
|
|
// Set query parameters to route if required
|
|
|
|
const s = (x: number) => x.toFixed(6);
|
|
|
|
const bounds = `${s(minLat)},${s(maxLat)},${s(minLon)},${s(maxLon)}`;
|
|
|
|
const zoom = map.mapObject.getZoom().toString();
|
|
|
|
if (this.$route.query.b === bounds && this.$route.query.z === zoom) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.$router.replace({ query: { b: bounds, z: zoom } });
|
|
|
|
|
2023-02-08 18:52:53 +00:00
|
|
|
// Show clusters correctly while draging the map
|
2023-02-08 21:35:42 +00:00
|
|
|
const query = new URLSearchParams();
|
2023-02-08 21:53:38 +00:00
|
|
|
query.set("bounds", bounds);
|
2023-02-08 21:35:42 +00:00
|
|
|
query.set("zoom", zoom);
|
2023-02-08 03:59:04 +00:00
|
|
|
|
2023-02-08 21:35:42 +00:00
|
|
|
// Make API call
|
2023-02-08 22:13:13 +00:00
|
|
|
const url = API.Q(API.MAP_CLUSTERS(), query);
|
2023-02-08 03:59:04 +00:00
|
|
|
const res = await axios.get(url);
|
|
|
|
this.clusters = res.data;
|
2023-01-25 18:41:55 +00:00
|
|
|
},
|
2023-02-09 05:55:12 +00:00
|
|
|
|
|
|
|
clusterPreviewUrl(cluster: IMarkerCluster) {
|
2023-02-09 16:25:37 +00:00
|
|
|
let url = API.MAP_CLUSTER_PREVIEW(cluster.id);
|
|
|
|
if (cluster.u) {
|
|
|
|
url += `?u=${cluster.u}`;
|
|
|
|
}
|
|
|
|
return url;
|
2023-02-09 05:55:12 +00:00
|
|
|
},
|
2023-02-09 07:15:48 +00:00
|
|
|
|
|
|
|
zoomTo(center: [number, number]) {
|
|
|
|
const map = this.$refs.map as LMap;
|
2023-02-09 07:36:31 +00:00
|
|
|
const zoom = map.mapObject.getZoom() + 2;
|
2023-02-09 07:15:48 +00:00
|
|
|
map.mapObject.setView(center, zoom, { animate: true });
|
|
|
|
},
|
2023-01-26 04:51:42 +00:00
|
|
|
},
|
2023-01-25 18:41:55 +00:00
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2023-02-08 22:13:13 +00:00
|
|
|
.map-matter {
|
2023-02-08 21:35:42 +00:00
|
|
|
height: 100%;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.map {
|
|
|
|
height: 100%;
|
|
|
|
width: 100%;
|
|
|
|
margin: 0;
|
|
|
|
z-index: 0;
|
2023-01-25 18:41:55 +00:00
|
|
|
}
|
2023-02-09 05:55:12 +00:00
|
|
|
|
|
|
|
.preview {
|
|
|
|
width: 48px;
|
|
|
|
height: 48px;
|
2023-02-09 16:25:37 +00:00
|
|
|
background-color: rgba(0, 0, 0, 0.3);
|
2023-02-09 05:55:12 +00:00
|
|
|
border-radius: 5px;
|
|
|
|
position: relative;
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
transform: scale(1.8);
|
|
|
|
}
|
|
|
|
|
|
|
|
img {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
object-fit: cover;
|
|
|
|
border-radius: 5px;
|
2023-02-09 07:15:48 +00:00
|
|
|
cursor: pointer;
|
2023-02-09 05:55:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.count {
|
|
|
|
position: absolute;
|
|
|
|
top: 0;
|
|
|
|
right: 0;
|
|
|
|
background-color: var(--color-primary-default);
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
padding: 0 4px;
|
|
|
|
border-radius: 5px;
|
|
|
|
font-size: 0.8em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<style lang="scss">
|
2023-02-09 07:55:54 +00:00
|
|
|
.leaflet-marker-icon {
|
|
|
|
transition: transform 0.2s;
|
|
|
|
animation: fade-in 0.2s;
|
|
|
|
}
|
|
|
|
|
2023-02-09 05:55:12 +00:00
|
|
|
// Show leaflet marker on top on hover
|
|
|
|
.leaflet-marker-icon:hover {
|
|
|
|
z-index: 100000 !important;
|
|
|
|
}
|
2023-02-09 07:55:54 +00:00
|
|
|
|
|
|
|
@keyframes fade-in {
|
|
|
|
0% {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
100% {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
}
|
2023-01-26 04:51:42 +00:00
|
|
|
</style>
|