first init
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import maplibregl, { type MapOptions } from "maplibre-gl";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Minus, MapPin, Compass, Expand, X } from "@lucide/vue";
|
||||
import type { FeatureCollection, Point } from "geojson";
|
||||
import { cn } from "@mykopkb/core/utils/cn";
|
||||
import { map } from "@mykopkb/core/styles/map.styles";
|
||||
|
||||
const mapRef = ref();
|
||||
const mapInstance = ref<maplibregl.Map>();
|
||||
const isFullscreen = ref(false);
|
||||
|
||||
const {
|
||||
class: className,
|
||||
markers,
|
||||
...props
|
||||
} = defineProps<
|
||||
Partial<MapOptions> & {
|
||||
class?: string;
|
||||
markers?: FeatureCollection<Point>;
|
||||
}
|
||||
>();
|
||||
|
||||
onMounted(() => {
|
||||
mapInstance.value = new maplibregl.Map({
|
||||
...Object.fromEntries(Object.entries(props).filter(([_, value]) => value)),
|
||||
style: "https://tiles.openfreemap.org/styles/positron",
|
||||
container: mapRef.value,
|
||||
});
|
||||
|
||||
markers &&
|
||||
mapInstance.value.on("load", () => {
|
||||
// Add source with clustering
|
||||
mapInstance.value!.addSource("markers", {
|
||||
type: "geojson",
|
||||
data: markers,
|
||||
cluster: true,
|
||||
clusterMaxZoom: 14, // Max zoom for clustering
|
||||
clusterRadius: 50, // Cluster radius in pixels
|
||||
});
|
||||
|
||||
// Layer for clusters (circles)
|
||||
mapInstance.value!.addLayer({
|
||||
id: "clusters",
|
||||
type: "circle",
|
||||
source: "markers",
|
||||
filter: ["has", "point_count"],
|
||||
paint: {
|
||||
"circle-color": [
|
||||
"step",
|
||||
["get", "point_count"],
|
||||
"#cccccc", // Color for < 5 points
|
||||
5,
|
||||
"#cccccc", // Color for 5-10 points
|
||||
10,
|
||||
"#cccccc", // Color for > 10 points
|
||||
],
|
||||
"circle-radius": [
|
||||
"step",
|
||||
["get", "point_count"],
|
||||
20, // Radius for < 5 points
|
||||
5,
|
||||
30, // Radius for 5-10 points
|
||||
10,
|
||||
40, // Radius for > 10 points
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Layer for cluster count numbers
|
||||
mapInstance.value!.addLayer({
|
||||
id: "cluster-count",
|
||||
type: "symbol",
|
||||
source: "markers",
|
||||
filter: ["has", "point_count"],
|
||||
layout: {
|
||||
"text-field": "{point_count_abbreviated}",
|
||||
"text-font": ["Open Sans Bold"],
|
||||
"text-size": 12,
|
||||
},
|
||||
});
|
||||
|
||||
// Layer for individual points (unclustered)
|
||||
mapInstance.value!.addLayer({
|
||||
id: "unclustered-point",
|
||||
type: "circle",
|
||||
source: "markers",
|
||||
filter: ["!", ["has", "point_count"]],
|
||||
paint: {
|
||||
"circle-color": "#333333",
|
||||
"circle-radius": 8,
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
},
|
||||
});
|
||||
|
||||
// Click on cluster to zoom in
|
||||
mapInstance.value!.on("click", "clusters", async (e) => {
|
||||
const features = mapInstance.value!.queryRenderedFeatures(e.point, {
|
||||
layers: ["clusters"],
|
||||
});
|
||||
|
||||
const clusterId = features[0]?.properties.cluster_id;
|
||||
const source = mapInstance.value!.getSource(
|
||||
"markers"
|
||||
) as maplibregl.GeoJSONSource;
|
||||
|
||||
try {
|
||||
const zoom = await source.getClusterExpansionZoom(clusterId);
|
||||
|
||||
mapInstance.value!.easeTo({
|
||||
center: (features[0]?.geometry as any).coordinates,
|
||||
zoom: zoom,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting cluster expansion zoom:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// Click on individual point to show popup
|
||||
mapInstance.value!.on("click", "unclustered-point", (e) => {
|
||||
const coordinates = (
|
||||
e.features![0]?.geometry as any
|
||||
).coordinates.slice();
|
||||
const { name, type } = (e.features![0]?.properties || {}) as any;
|
||||
|
||||
new maplibregl.Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(`<h3>${name}</h3><p>${type}</p>`)
|
||||
.addTo(mapInstance.value!);
|
||||
});
|
||||
|
||||
// Change cursor on hover cluster/point
|
||||
mapInstance.value!.on("mouseenter", "clusters", () => {
|
||||
mapInstance.value!.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
mapInstance.value!.on("mouseleave", "clusters", () => {
|
||||
mapInstance.value!.getCanvas().style.cursor = "";
|
||||
});
|
||||
mapInstance.value!.on("mouseenter", "unclustered-point", () => {
|
||||
mapInstance.value!.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
mapInstance.value!.on("mouseleave", "unclustered-point", () => {
|
||||
mapInstance.value!.getCanvas().style.cursor = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const zoomIn = () => {
|
||||
mapInstance.value?.zoomIn();
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
mapInstance.value?.zoomOut();
|
||||
};
|
||||
|
||||
const resetNorth = () => {
|
||||
mapInstance.value?.easeTo({ bearing: 0, pitch: 0 });
|
||||
};
|
||||
|
||||
const locateMe = () => {
|
||||
if (!navigator.geolocation) {
|
||||
alert("Geolocation is not supported by your browser");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { longitude, latitude } = position.coords;
|
||||
mapInstance.value?.flyTo({
|
||||
center: [longitude, latitude],
|
||||
zoom: 14,
|
||||
});
|
||||
|
||||
new maplibregl.Marker({ color: "var(--color-foreground)" })
|
||||
.setLngLat([longitude, latitude])
|
||||
.addTo(mapInstance.value!);
|
||||
},
|
||||
(error) => {
|
||||
alert("Failed to get location: " + error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!mapRef.value) return;
|
||||
|
||||
if (!isFullscreen.value) {
|
||||
if (mapRef.value.requestFullscreen) {
|
||||
mapRef.value.requestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-scope="map" data-part="root" ref="mapRef" :class="cn(map, className)">
|
||||
<div data-scope="map" data-part="controls">
|
||||
<Button data-scope="map" data-part="zoom-in" @click="zoomIn" variant="ghost" size="sm">
|
||||
<Plus />
|
||||
</Button>
|
||||
<Button data-scope="map" data-part="zoom-out" @click="zoomOut" variant="ghost" size="sm">
|
||||
<Minus />
|
||||
</Button>
|
||||
<Button data-scope="map" data-part="reset-north" @click="resetNorth" variant="ghost" size="sm">
|
||||
<Compass />
|
||||
</Button>
|
||||
<Button data-scope="map" data-part="locate" @click="locateMe" variant="ghost" size="sm">
|
||||
<MapPin />
|
||||
</Button>
|
||||
<Button data-scope="map" data-part="toggle-fullscreen" @click="toggleFullscreen" variant="ghost" size="sm">
|
||||
<Expand v-if="!isFullscreen" />
|
||||
<X v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Map } from "./Map.vue";
|
||||
Reference in New Issue
Block a user