Display pois
This commit is contained in:
parent
c157c2d5ec
commit
433fca64ad
6 changed files with 412 additions and 10 deletions
|
|
@ -2,14 +2,89 @@ import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
|||
import { Map as MapGL } from 'react-map-gl/maplibre';
|
||||
import DeckGL from '@deck.gl/react';
|
||||
import { H3HexagonLayer } from '@deck.gl/geo-layers';
|
||||
import { IconLayer } from '@deck.gl/layers';
|
||||
import type { PickingInfo } from '@deck.gl/core';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type { HexagonData, ViewState, ViewChangeParams, Bounds } from '../types';
|
||||
import type { HexagonData, ViewState, ViewChangeParams, Bounds, POI } from '../types';
|
||||
|
||||
interface MapProps {
|
||||
data: HexagonData[];
|
||||
pois: POI[];
|
||||
onViewChange: (params: ViewChangeParams) => void;
|
||||
}
|
||||
|
||||
// Twemoji CDN base URL
|
||||
const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/';
|
||||
|
||||
// Map category to Twemoji codepoint (emoji unicode -> hex)
|
||||
const POI_EMOJI_CODES: Record<string, string> = {
|
||||
// Schools
|
||||
elementary_school: '1f3eb', // 🏫
|
||||
school: '1f3eb',
|
||||
high_school: '1f393', // 🎓
|
||||
preschool: '1f476', // 👶
|
||||
college_university: '1f393',
|
||||
private_school: '1f3eb',
|
||||
// Healthcare
|
||||
doctor: '1f3e5', // 🏥
|
||||
dentist: '1f9b7', // 🦷
|
||||
pharmacy: '1f48a', // 💊
|
||||
hospital: '1f3e5',
|
||||
public_health_clinic: '1f3e5',
|
||||
// Transport
|
||||
train_station: '1f689', // 🚉
|
||||
bus_station: '1f68c', // 🚌
|
||||
metro_station: '1f687', // 🚇
|
||||
light_rail_and_subway_stations: '1f687',
|
||||
// Parks
|
||||
park: '1f333', // 🌳
|
||||
national_park: '1f3de', // 🏞
|
||||
dog_park: '1f415', // 🐕
|
||||
// Emergency
|
||||
police_department: '1f694', // 🚔
|
||||
fire_department: '1f692', // 🚒
|
||||
// Supermarkets
|
||||
supermarket: '1f6d2', // 🛒
|
||||
grocery_store: '1f6d2',
|
||||
convenience_store: '1f3ea', // 🏪
|
||||
};
|
||||
|
||||
function getPOIIconUrl(category: string): string {
|
||||
const code = POI_EMOJI_CODES[category] || '1f4cd'; // 📍 default
|
||||
return `${TWEMOJI_BASE}${code}.png`;
|
||||
}
|
||||
|
||||
// Tooltip emojis (these render fine in HTML)
|
||||
const TOOLTIP_EMOJIS: Record<string, string> = {
|
||||
elementary_school: '🏫',
|
||||
school: '🏫',
|
||||
high_school: '🎓',
|
||||
preschool: '👶',
|
||||
college_university: '🎓',
|
||||
private_school: '🏫',
|
||||
doctor: '👨⚕️',
|
||||
dentist: '🦷',
|
||||
pharmacy: '💊',
|
||||
hospital: '🏥',
|
||||
public_health_clinic: '🏥',
|
||||
train_station: '🚉',
|
||||
bus_station: '🚌',
|
||||
metro_station: '🚇',
|
||||
light_rail_and_subway_stations: '🚇',
|
||||
park: '🌳',
|
||||
national_park: '🏞️',
|
||||
dog_park: '🐕',
|
||||
police_department: '🚔',
|
||||
fire_department: '🚒',
|
||||
supermarket: '🛒',
|
||||
grocery_store: '🛒',
|
||||
convenience_store: '🏪',
|
||||
};
|
||||
|
||||
function getTooltipEmoji(category: string): string {
|
||||
return TOOLTIP_EMOJIS[category] || '📍';
|
||||
}
|
||||
|
||||
const INITIAL_VIEW: ViewState = {
|
||||
longitude: -1.5,
|
||||
latitude: 53.5,
|
||||
|
|
@ -118,7 +193,7 @@ interface Dimensions {
|
|||
height: number;
|
||||
}
|
||||
|
||||
export default function Map({ data, onViewChange }: MapProps) {
|
||||
export default function Map({ data, pois, onViewChange }: MapProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW);
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
|
||||
|
|
@ -154,6 +229,27 @@ export default function Map({ data, onViewChange }: MapProps) {
|
|||
setViewState(newViewState);
|
||||
}, []);
|
||||
|
||||
// Popup state for POI hover (using screen coordinates)
|
||||
const [popupInfo, setPopupInfo] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
name: string;
|
||||
category: string;
|
||||
} | null>(null);
|
||||
|
||||
const handlePoiHover = useCallback((info: PickingInfo<POI>) => {
|
||||
if (info.object && info.x !== undefined && info.y !== undefined) {
|
||||
setPopupInfo({
|
||||
x: info.x,
|
||||
y: info.y,
|
||||
name: info.object.name,
|
||||
category: info.object.category,
|
||||
});
|
||||
} else {
|
||||
setPopupInfo(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const layers = useMemo(
|
||||
() => [
|
||||
new H3HexagonLayer<HexagonData>({
|
||||
|
|
@ -166,20 +262,76 @@ export default function Map({ data, onViewChange }: MapProps) {
|
|||
opacity: 0.5,
|
||||
highPrecision: true,
|
||||
}),
|
||||
new IconLayer<POI>({
|
||||
id: 'poi-icons',
|
||||
data: pois,
|
||||
getPosition: (d) => [d.lng, d.lat],
|
||||
getIcon: (d) => ({
|
||||
url: getPOIIconUrl(d.category),
|
||||
width: 72,
|
||||
height: 72,
|
||||
}),
|
||||
getSize: 24,
|
||||
sizeMinPixels: 20,
|
||||
sizeMaxPixels: 40,
|
||||
pickable: true,
|
||||
onHover: handlePoiHover,
|
||||
}),
|
||||
],
|
||||
[data]
|
||||
[data, pois, handlePoiHover]
|
||||
);
|
||||
|
||||
|
||||
// Tooltip for hexagons only (POIs use MapLibre popup)
|
||||
const getTooltip = useCallback(({ object }: { object?: HexagonData }) => {
|
||||
if (!object || !('h3' in object)) return null;
|
||||
|
||||
const hex = object as HexagonData;
|
||||
return {
|
||||
html: `<div style="padding: 8px; font-size: 14px;">
|
||||
<strong>Avg: £${hex.avg_price?.toLocaleString() || 'N/A'}</strong>
|
||||
<div style="color: #666; font-size: 12px;">
|
||||
${hex.count} sales<br/>
|
||||
Range: £${hex.min_price?.toLocaleString()} - £${hex.max_price?.toLocaleString()}
|
||||
</div>
|
||||
</div>`,
|
||||
style: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full" ref={containerRef}>
|
||||
<div className="flex-1 h-full relative" ref={containerRef}>
|
||||
<DeckGL
|
||||
viewState={viewState}
|
||||
controller
|
||||
layers={layers}
|
||||
onViewStateChange={handleViewStateChange as never}
|
||||
getTooltip={getTooltip as never}
|
||||
>
|
||||
<MapGL mapStyle={MAP_STYLE} />
|
||||
</DeckGL>
|
||||
{popupInfo && (
|
||||
<div
|
||||
className="absolute pointer-events-none bg-white rounded shadow-lg p-2 text-sm"
|
||||
style={{
|
||||
left: popupInfo.x,
|
||||
top: popupInfo.y - 40,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<strong>
|
||||
{getTooltipEmoji(popupInfo.category)} {popupInfo.name}
|
||||
</strong>
|
||||
<div className="text-gray-500 text-xs">
|
||||
{popupInfo.category.replace(/_/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue