322 lines
9.1 KiB
Vue
322 lines
9.1 KiB
Vue
<template>
|
|
<div
|
|
ref="maps"
|
|
:class="wrapperClasses.concat(' leafleat-wrapper-class')"
|
|
>
|
|
<div
|
|
ref="marker"
|
|
class="d-none marker-pin bg-white text-primary px-3 py-1 d-flex justify-content-center align-items-center f-barlow rounded-pill border border-1 border-primary"
|
|
>
|
|
<img
|
|
class="marker-logo mx-1"
|
|
src=""
|
|
alt=""
|
|
>
|
|
<span>
|
|
<span class="mx-1 marker-title fw-bold text-uppercase text-break" />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { DivIcon, Icon, Map, Marker, TileLayer, type LatLngExpression, type LatLngTuple } from "leaflet";
|
|
// @ts-ignore
|
|
import "overlapping-marker-spiderfier-leaflet";
|
|
import type { OverlappingMarkerSpiderfier } from "ts-overlapping-marker-spiderfier";
|
|
// @ts-ignore
|
|
import "leaflet.fullscreen";
|
|
import { defineComponent } from "vue";
|
|
import trans from "../../modules/trans";
|
|
|
|
type Site = {
|
|
name: string;
|
|
slug: string;
|
|
/** WYSIWYG text */
|
|
description: string;
|
|
employees: number;
|
|
coords: {
|
|
lat: number,
|
|
lng: number,
|
|
},
|
|
logo: {
|
|
src: string,
|
|
alt: string;
|
|
title: string;
|
|
},
|
|
divIcon: DivIcon,
|
|
pinIcon: DivIcon,
|
|
marker: Marker|null
|
|
};
|
|
|
|
export default defineComponent({
|
|
name: "LeafletMapVueComponent",
|
|
mixins: [trans],
|
|
data(): {
|
|
wrapperClasses: string[];
|
|
map: Map;
|
|
oms: OverlappingMarkerSpiderfier;
|
|
initialPosition: LatLngExpression | null;
|
|
initialZoom: number;
|
|
siteList: Array<Site>;
|
|
markerTextLength: number;
|
|
controlSliderId: string|null;
|
|
} {
|
|
return {
|
|
wrapperClasses:[],
|
|
map: {} as Map,
|
|
oms: {} as OverlappingMarkerSpiderfier,
|
|
initialPosition: null,
|
|
initialZoom: 9,
|
|
siteList: [],
|
|
markerTextLength: 20,
|
|
controlSliderId: null
|
|
};
|
|
},
|
|
computed: {},
|
|
mounted() {
|
|
const json = String(this.$attrs.json ?? "{}"),
|
|
data = JSON.parse(json);
|
|
this.wrapperClasses = data.wrapperClasses ?? [];
|
|
|
|
if(data.coordinates) {
|
|
this.initialPosition = [
|
|
Number.parseFloat(data.coordinates.lat),
|
|
Number.parseFloat(data.coordinates.lng)
|
|
];
|
|
}
|
|
if(data.initialZoom) {
|
|
this.initialZoom = data.initialZoom;
|
|
}
|
|
|
|
if(data.controlSliderId) {
|
|
this.controlSliderId = String(data.controlSliderId);
|
|
}
|
|
|
|
this.parseSiteModelList(data.siteModelList);
|
|
|
|
this.$nextTick(() => {
|
|
this.displayMap();
|
|
this.setupMarkers();
|
|
});
|
|
},
|
|
methods: {
|
|
displayMap() {
|
|
const mapWrapper = this.$refs.maps as HTMLDivElement;
|
|
this.map = new Map(mapWrapper, {
|
|
// @ts-ignore
|
|
fullscreenControl: true,
|
|
fullscreenControlOptions: {
|
|
position: "topleft"
|
|
}
|
|
});
|
|
(new TileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: "© <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>"
|
|
})).addTo(this.map as Map);
|
|
if(!this.initialPosition) {
|
|
// * Auto set viewbox
|
|
(this.map as Map).fitBounds(this.siteList.map(site => Object.values(site.coords)) as LatLngTuple[]);
|
|
} else {
|
|
(this.map as Map).setView(this.initialPosition, this.initialZoom);
|
|
}
|
|
// * Setup OMS
|
|
// @ts-ignore
|
|
this.oms = new OverlappingMarkerSpiderfier(this.map as Map, {
|
|
// spiderfy markers within this range of the one clicked, in px
|
|
nearbyDistance: 120,
|
|
// circleSpiralSwitchover: 250,
|
|
/**
|
|
* show spiral instead of circle from this marker count upwards
|
|
* 0 -> always spiral; Infinity -> always circle
|
|
*/
|
|
circleSpiralSwitchover: 12,
|
|
// related to size of spiral (experiment!)
|
|
circleFootSeparation: 80,
|
|
// related to size of spiral (experiment!)
|
|
circleStartAngle: (Math.PI * 2) / 24,
|
|
// ditto
|
|
spiralLengthStart: 130,
|
|
// ditto
|
|
spiralLengthFactor: 5,
|
|
});
|
|
this.oms.addListener("click", this.selectedMarker);
|
|
this.oms.addListener("spiderfy", this.clearSelectedMarkers);
|
|
this.map.on("zoomend", this.updateMarkers);
|
|
},
|
|
parseSiteModelList(siteModelList: LaravelModelList) {
|
|
const bodyFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
siteModelList.forEach((site) => {
|
|
this.siteList.push({
|
|
name: String(site.name),
|
|
slug: String(site.slug),
|
|
description: String(site.description),
|
|
employees: Number.parseInt(String(site.employees)),
|
|
coords: {
|
|
lat: Number.parseFloat(String(site.latitude)),
|
|
lng: Number.parseFloat(String(site.longitude)),
|
|
},
|
|
logo: {
|
|
src: String(site.logo),
|
|
alt: String(site.logo_alt),
|
|
title: String(site.logo_title),
|
|
},
|
|
marker: null,
|
|
divIcon: new DivIcon({
|
|
className: "leaf-div-icon",
|
|
html: this.fetchNewMarkerElement(
|
|
String(site.name),
|
|
{
|
|
src: String(site.logo),
|
|
alt: String(site.logo_alt),
|
|
title: String(site.logo_title),
|
|
}
|
|
),
|
|
iconSize: [10*bodyFontSize, 3*bodyFontSize],
|
|
}),
|
|
pinIcon: new DivIcon({
|
|
html: "<i class=\"fa-solid fa-location-dot\" style=\"font-size:24px; color: #103251;\"></i>",
|
|
className: "",
|
|
iconSize: [24, 18]
|
|
})
|
|
});
|
|
});
|
|
},
|
|
fetchNewMarkerElement(
|
|
title: string,
|
|
logo: {src: string,alt: string,title: string}
|
|
): HTMLDivElement {
|
|
const marker = (this.$refs.marker as HTMLDivElement).cloneNode(true) as HTMLDivElement;
|
|
const logoEl = marker.querySelector(".marker-logo") as HTMLImageElement|null;
|
|
marker.querySelector(".marker-title")!.textContent = this.truncateString(title, this.markerTextLength);
|
|
logoEl!.src = `/${logo.src}`;
|
|
logoEl!.alt = logo.alt;
|
|
logoEl!.title = logo.title;
|
|
marker.classList.remove("d-none");
|
|
return marker;
|
|
},
|
|
setupMarkers() {
|
|
this.siteList.forEach((site) => {
|
|
const mapZoom = this.map.getZoom();
|
|
const marker = new Marker(site.coords, {
|
|
title: site.name,
|
|
alt: site.name,
|
|
icon: mapZoom >= 8 ? site.pinIcon as Icon : site.divIcon as Icon
|
|
});
|
|
this.map.addLayer(marker);
|
|
// @ts-ignore
|
|
this.oms.addMarker(marker);
|
|
site.marker = marker;
|
|
});
|
|
},
|
|
updateMarkers() {
|
|
this.siteList.forEach((site) => {
|
|
let newMarker = null;
|
|
const mapZoom = this.map.getZoom();
|
|
|
|
if (!site.marker) {
|
|
throw new Error("Missing Marker");
|
|
}
|
|
|
|
if (mapZoom >= 12) {
|
|
newMarker = site.divIcon;
|
|
} else {
|
|
newMarker = site.pinIcon;
|
|
}
|
|
|
|
site.marker.setIcon(newMarker as Icon);
|
|
});
|
|
},
|
|
truncateString(str: string, limit: number, end: string = "..") {
|
|
if (str.length <= limit) {
|
|
return str;
|
|
}
|
|
return str.slice(0, limit) + end;
|
|
},
|
|
selectedMarker(marker: Marker) {
|
|
this.clearSelectedMarkers();
|
|
const siteIndex = this.siteList.findIndex(site => site!.marker!.options.title === marker.options.title);
|
|
if(siteIndex === -1) {
|
|
throw new Error("Could not fetch related Site");
|
|
}
|
|
const pin = marker.getElement()!.firstChild as HTMLElement | null;
|
|
|
|
if(!pin) {
|
|
throw new Error("Missing Marker Pin");
|
|
}
|
|
|
|
if (pin.tagName !== "svg") {
|
|
pin.classList.add("bg-primary", "text-white");
|
|
pin.classList.remove("bg-white", "text-primary");
|
|
}
|
|
|
|
if(this.controlSliderId) {
|
|
const slider = window.__SLIDERS.get(this.controlSliderId);
|
|
if(!slider) {
|
|
throw new Error(`Missing slider to control ${this.controlSliderId}`);
|
|
}
|
|
slider.swiper.slideTo(siteIndex);
|
|
}
|
|
},
|
|
clearSelectedMarkers() {
|
|
this.siteList.forEach(site => {
|
|
const pin = site.marker!.getElement()!.firstChild as HTMLElement | null;
|
|
if(!pin) {
|
|
throw new Error("Missing Marker Pin");
|
|
}
|
|
|
|
if (pin.tagName !== "svg") {
|
|
pin.classList.add("bg-white", "text-primary");
|
|
pin.classList.remove("bg-primary", "text-white");
|
|
}
|
|
});
|
|
}
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
@import "../../../../node_modules/leaflet.fullscreen/Control.FullScreen.css";
|
|
</style>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "bootstrap/scss/functions";
|
|
@import "bootstrap/scss/variables";
|
|
@import "bootstrap/scss/variables-dark";
|
|
@import "bootstrap/scss/maps";
|
|
@import "bootstrap/scss/mixins";
|
|
@import "bootstrap/scss/vendor/rfs";
|
|
.leafleat-wrapper-class {
|
|
min-height: 30rem;
|
|
|
|
@include media-breakpoint-up(sm) {
|
|
min-height: 35rem;
|
|
}
|
|
|
|
@include media-breakpoint-up(md) {
|
|
min-height: 40rem;
|
|
}
|
|
|
|
iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.marker-pin {
|
|
width: 10rem;
|
|
height: 3rem;
|
|
}
|
|
.marker-logo {
|
|
object-fit: contain;
|
|
height: auto;
|
|
width: 2rem;
|
|
}
|
|
.marker-title {
|
|
@include rfs(0.8rem, font-size);
|
|
overflow: hidden;
|
|
display: -webkit-box;
|
|
/* number of lines to show */
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
}
|
|
</style>
|