Files
My-KOPKB/fe/src/docs/pages/map.vue
T
ismailmasseran 421775d3ff
Build Docker Image / build-backend (push) Successful in 46s
Build Docker Image / build-frontend (push) Failing after 11s
Dev/v1.1 (#2)
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #2
2026-07-02 10:09:08 +08:00

370 lines
10 KiB
Vue

<script lang="ts" setup>
import { Map } from "@/components/ui/map";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
const markersData = {
type: "FeatureCollection" as const,
features: [
{
type: "Feature" as const,
properties: { name: "Cafe Berlin", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.388, 52.517],
},
},
{
type: "Feature" as const,
properties: { name: "Restaurant Alex", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.39, 52.518],
},
},
{
type: "Feature" as const,
properties: { name: "Coffee House", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.385, 52.515],
},
},
{
type: "Feature" as const,
properties: { name: "Pizza Place", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.392, 52.519],
},
},
{
type: "Feature" as const,
properties: { name: "Burger Joint", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.387, 52.516],
},
},
{
type: "Feature" as const,
properties: { name: "Starbucks", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.391, 52.52],
},
},
{
type: "Feature" as const,
properties: { name: "Sushi Bar", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.395, 52.522],
},
},
{
type: "Feature" as const,
properties: { name: "Bakery", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.383, 52.514],
},
},
{
type: "Feature" as const,
properties: { name: "Italian Restaurant", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.398, 52.525],
},
},
{
type: "Feature" as const,
properties: { name: "Tea House", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.38, 52.512],
},
},
],
};
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
</template>
<template #code>
<PreviewCode>
{{ `
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add maplibre-gl</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/map/Map.vue">
{{`
<${""}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>
` }}
</PreviewCode>
<PreviewCode title="components/ui/map/index.ts">
{{ `
export { default as Map } from "./Map.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Map } from "@/components/ui/map";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
` }}
</PreviewCode>
</div>
</template>