Modulo makers3d desarrollado con codex V 0.0.1
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { divIcon, type DivIcon, type Map as LeafletMap } from "leaflet";
|
||||
import { Circle, CircleMarker, MapContainer, Marker, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
|
||||
|
||||
type MakerMarker = {
|
||||
id: string;
|
||||
slug: string;
|
||||
businessName: string;
|
||||
city: string;
|
||||
province: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
|
||||
type DisplayMarker = MakerMarker & {
|
||||
displayLatitude: number;
|
||||
displayLongitude: number;
|
||||
radius: number;
|
||||
};
|
||||
|
||||
type ClusterGroup = {
|
||||
key: string;
|
||||
count: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
markers: DisplayMarker[];
|
||||
};
|
||||
|
||||
const cordobaCenter: [number, number] = [-31.4201, -64.1888];
|
||||
const userApproximationRadius = 900;
|
||||
|
||||
function hashValue(input: string, seed: number) {
|
||||
let value = seed;
|
||||
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
value = (value * 33 + input.charCodeAt(index) + index) % 1000003;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function toOffset(value: number, spread: number) {
|
||||
return ((value % 1000) / 999 - 0.5) * spread;
|
||||
}
|
||||
|
||||
function toDisplayMarkers(markers: MakerMarker[]): DisplayMarker[] {
|
||||
return markers.map((marker) => {
|
||||
const latSeed = hashValue(marker.id, 17);
|
||||
const lngSeed = hashValue(marker.slug, 29);
|
||||
const radiusSeed = hashValue(marker.businessName, 43);
|
||||
|
||||
return {
|
||||
...marker,
|
||||
displayLatitude: marker.latitude + toOffset(latSeed, 0.0072),
|
||||
displayLongitude: marker.longitude + toOffset(lngSeed, 0.0094),
|
||||
radius: 8 + (radiusSeed % 4)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getClusterRadius(zoom: number) {
|
||||
if (zoom >= 15) {
|
||||
return 0;
|
||||
}
|
||||
if (zoom >= 14) {
|
||||
return 34;
|
||||
}
|
||||
if (zoom >= 13) {
|
||||
return 42;
|
||||
}
|
||||
if (zoom >= 12) {
|
||||
return 50;
|
||||
}
|
||||
if (zoom >= 11) {
|
||||
return 58;
|
||||
}
|
||||
return 66;
|
||||
}
|
||||
|
||||
function buildClusterGroups(markers: DisplayMarker[], map: LeafletMap, zoom: number) {
|
||||
const clusterRadius = getClusterRadius(zoom);
|
||||
|
||||
if (clusterRadius === 0) {
|
||||
return markers.map((marker) => ({
|
||||
key: marker.id,
|
||||
count: 1,
|
||||
latitude: marker.displayLatitude,
|
||||
longitude: marker.displayLongitude,
|
||||
markers: [marker]
|
||||
}));
|
||||
}
|
||||
|
||||
const projectedMarkers = markers.map((marker) => ({
|
||||
marker,
|
||||
point: map.project([marker.displayLatitude, marker.displayLongitude], zoom)
|
||||
}));
|
||||
const visited = new Set<number>();
|
||||
const groups: ClusterGroup[] = [];
|
||||
|
||||
for (let index = 0; index < projectedMarkers.length; index += 1) {
|
||||
if (visited.has(index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const queue = [index];
|
||||
const memberIndexes: number[] = [];
|
||||
visited.add(index);
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentIndex = queue.shift() as number;
|
||||
const currentMarker = projectedMarkers[currentIndex];
|
||||
memberIndexes.push(currentIndex);
|
||||
|
||||
for (let candidateIndex = 0; candidateIndex < projectedMarkers.length; candidateIndex += 1) {
|
||||
if (visited.has(candidateIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateMarker = projectedMarkers[candidateIndex];
|
||||
const deltaX = currentMarker.point.x - candidateMarker.point.x;
|
||||
const deltaY = currentMarker.point.y - candidateMarker.point.y;
|
||||
|
||||
if (Math.hypot(deltaX, deltaY) <= clusterRadius) {
|
||||
visited.add(candidateIndex);
|
||||
queue.push(candidateIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bucket = memberIndexes.map((memberIndex) => projectedMarkers[memberIndex].marker);
|
||||
const latitude = bucket.reduce((sum, marker) => sum + marker.displayLatitude, 0) / bucket.length;
|
||||
const longitude = bucket.reduce((sum, marker) => sum + marker.displayLongitude, 0) / bucket.length;
|
||||
|
||||
groups.push({
|
||||
key: bucket.map((marker) => marker.id).sort().join(":"),
|
||||
count: bucket.length,
|
||||
latitude,
|
||||
longitude,
|
||||
markers: bucket
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function buildClusterIcon(count: number): DivIcon {
|
||||
const size = count >= 10 ? 60 : count >= 5 ? 54 : 48;
|
||||
const toneClass = count >= 10 ? "is-large" : count >= 5 ? "is-medium" : "is-small";
|
||||
|
||||
return divIcon({
|
||||
className: "map-home-cluster-icon-shell",
|
||||
html: `<span class="map-home-cluster-badge ${toneClass}">${count}</span>`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2]
|
||||
});
|
||||
}
|
||||
|
||||
function FitMapToMarkers({ markers }: { markers: DisplayMarker[] }) {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
if (markers.length === 0) {
|
||||
map.setView(cordobaCenter, 12);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = markers.map((marker) => [marker.displayLatitude, marker.displayLongitude] as [number, number]);
|
||||
map.fitBounds(bounds, {
|
||||
padding: [40, 40],
|
||||
maxZoom: 14
|
||||
});
|
||||
}, [map, markers]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ClusteredMarkerLayer({ markers }: { markers: DisplayMarker[] }) {
|
||||
const map = useMap();
|
||||
const [zoom, setZoom] = useState(() => map.getZoom());
|
||||
|
||||
useMapEvents({
|
||||
zoomend() {
|
||||
setZoom(map.getZoom());
|
||||
}
|
||||
});
|
||||
|
||||
const groups = buildClusterGroups(markers, map, zoom);
|
||||
|
||||
return (
|
||||
<>
|
||||
{groups.map((group) => {
|
||||
if (group.count === 1) {
|
||||
const marker = group.markers[0];
|
||||
|
||||
return (
|
||||
<CircleMarker
|
||||
key={marker.id}
|
||||
center={[marker.displayLatitude, marker.displayLongitude]}
|
||||
radius={marker.radius}
|
||||
pathOptions={{
|
||||
color: "#ffffff",
|
||||
weight: 2,
|
||||
fillColor: "#10358d",
|
||||
fillOpacity: 0.95
|
||||
}}
|
||||
>
|
||||
<Popup className="map-home-popup" closeButton={false} offset={[0, -8]}>
|
||||
<div className="map-home-popup-card">
|
||||
<strong>{marker.businessName}</strong>
|
||||
<span>{marker.city}, {marker.province}</span>
|
||||
<a href={`/makers/${marker.slug}`}>Ver maker</a>
|
||||
</div>
|
||||
</Popup>
|
||||
</CircleMarker>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={group.key}
|
||||
position={[group.latitude, group.longitude]}
|
||||
icon={buildClusterIcon(group.count)}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
map.fitBounds(
|
||||
group.markers.map((marker) => [marker.displayLatitude, marker.displayLongitude] as [number, number]),
|
||||
{
|
||||
padding: [48, 48],
|
||||
maxZoom: 15,
|
||||
animate: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popup className="map-home-popup" closeButton={false} offset={[0, -8]}>
|
||||
<div className="map-home-popup-card">
|
||||
<strong>{group.count} makers en esta zona</strong>
|
||||
<span>Toca el grupo para acercar y separarlos.</span>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisterMapInstance({ mapRef }: { mapRef: MutableRefObject<LeafletMap | null> }) {
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
mapRef.current = map;
|
||||
|
||||
return () => {
|
||||
if (mapRef.current === map) {
|
||||
mapRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [map, mapRef]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function InteractiveMakerMap({ markers }: { markers: MakerMarker[] }) {
|
||||
const mapRef = useRef<LeafletMap | null>(null);
|
||||
const displayMarkers = toDisplayMarkers(markers);
|
||||
|
||||
const focusMarkers = () => {
|
||||
const map = mapRef.current;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (markers.length === 0) {
|
||||
map.setView(cordobaCenter, 12, { animate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
map.fitBounds(
|
||||
displayMarkers.map((marker) => [marker.displayLatitude, marker.displayLongitude] as [number, number]),
|
||||
{
|
||||
padding: [40, 40],
|
||||
maxZoom: 14,
|
||||
animate: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="map-home-map">
|
||||
<div className="map-home-realmap">
|
||||
<MapContainer
|
||||
center={cordobaCenter}
|
||||
zoom={13}
|
||||
scrollWheelZoom
|
||||
className="map-home-leaflet"
|
||||
>
|
||||
<RegisterMapInstance mapRef={mapRef} />
|
||||
|
||||
<TileLayer
|
||||
attribution='© OpenStreetMap contributors © CARTO'
|
||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
||||
/>
|
||||
|
||||
<FitMapToMarkers markers={displayMarkers} />
|
||||
|
||||
<Circle
|
||||
center={cordobaCenter}
|
||||
radius={userApproximationRadius}
|
||||
pathOptions={{
|
||||
color: "#1d67ff",
|
||||
weight: 2,
|
||||
fillColor: "#2e7cff",
|
||||
fillOpacity: 0.1
|
||||
}}
|
||||
/>
|
||||
|
||||
<CircleMarker
|
||||
center={cordobaCenter}
|
||||
radius={8}
|
||||
pathOptions={{
|
||||
color: "#ffffff",
|
||||
weight: 2,
|
||||
fillColor: "#2e7cff",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
/>
|
||||
|
||||
<ClusteredMarkerLayer markers={displayMarkers} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
|
||||
<div className="map-home-map-overlay" aria-hidden="false">
|
||||
<button className="map-home-crosshair" type="button" aria-label="Recentrar mapa" onClick={focusMarkers}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="5.4" />
|
||||
<path d="M12 2v3" />
|
||||
<path d="M12 19v3" />
|
||||
<path d="M2 12h3" />
|
||||
<path d="M19 12h3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button className="map-home-area-cta" type="button" onClick={focusMarkers}>
|
||||
Buscar en esta area
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user