Files
japan/src/pages/MapPage.tsx
2026-03-21 04:59:39 +00:00

232 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap, Polyline } from 'react-leaflet';
import L from 'leaflet';
import { Place, GeoPosition } from '../types';
import scheduleData from '../data/schedule.json';
import 'leaflet/dist/leaflet.css';
interface MapPageProps {
places: Place[];
position: GeoPosition | null;
selectedPlace: Place | null;
onSelectPlace: (place: Place) => void;
showRouteForDay: number | null;
onClearRoute: () => void;
}
// Fix marker icons
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
});
const createIcon = (emoji: string, isSelected: boolean = false) => {
return L.divIcon({
className: 'custom-marker',
html: `
<div style="
font-size: ${isSelected ? '28px' : '22px'};
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
transform: translate(-50%, -50%);
transition: all 0.2s;
${isSelected ? 'filter: drop-shadow(0 0 8px #FF6B6B);' : ''}
">
${emoji}
</div>
`,
iconSize: [40, 40],
iconAnchor: [20, 20],
});
};
const getCategoryEmoji = (category: string): string => {
const emojis: Record<string, string> = {
sight: '🏯',
restaurant: '🍜',
coffee: '☕',
snack: '🍡',
hotel: '🏨',
};
return emojis[category] || '📍';
};
function MapController({ center, zoom }: { center: [number, number]; zoom: number }) {
const map = useMap();
useEffect(() => {
map.flyTo(center, zoom, { duration: 0.5 });
}, [center, zoom, map]);
return null;
}
function LocationMarker({ position }: { position: GeoPosition | null }) {
if (!position) return null;
return (
<Marker
position={[position.lat, position.lng]}
icon={L.divIcon({
className: 'location-marker',
html: `
<div style="
width: 16px;
height: 16px;
background: #4285F4;
border: 3px solid white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.5);
animation: pulse 2s infinite;
"></div>
`,
iconSize: [22, 22],
iconAnchor: [11, 11],
})}
/>
);
}
export function MapPage({ places, position, selectedPlace, onSelectPlace, showRouteForDay, onClearRoute }: MapPageProps) {
const [mapCenter, setMapCenter] = useState<[number, number]>([35.6762, 139.6503]);
const [mapZoom, setMapZoom] = useState(11);
const schedule = scheduleData as Record<string, any>;
// Get route for selected day
const routeCoordinates = useMemo(() => {
if (!showRouteForDay) return [];
const dayData = schedule[`day${showRouteForDay}`];
if (!dayData) return [];
const coords: [number, number][] = [];
dayData.schedule.forEach((item: any) => {
if (item.placeId) {
const place = places.find(p => p.id === item.placeId);
if (place) {
coords.push([place.lat, place.lng]);
}
}
});
return coords;
}, [showRouteForDay, places, schedule]);
// Update center when selected place changes
useEffect(() => {
if (selectedPlace) {
setMapCenter([selectedPlace.lat, selectedPlace.lng]);
setMapZoom(15);
}
}, [selectedPlace]);
// Update center for route
useEffect(() => {
if (routeCoordinates.length > 0) {
const bounds = L.latLngBounds(routeCoordinates);
const center = bounds.getCenter();
setMapCenter([center.lat, center.lng]);
setMapZoom(12);
}
}, [routeCoordinates]);
const visiblePlaces = useMemo(() => {
if (showRouteForDay) {
const dayData = schedule[`day${showRouteForDay}`];
if (!dayData) return places;
const placeIds = dayData.schedule
.filter((item: any) => item.placeId)
.map((item: any) => item.placeId);
return places.filter(p => placeIds.includes(p.id));
}
return places;
}, [places, showRouteForDay, schedule]);
return (
<div className="h-full relative">
<MapContainer
center={mapCenter}
zoom={mapZoom}
className="h-full w-full"
zoomControl={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
/>
<MapController center={mapCenter} zoom={mapZoom} />
<LocationMarker position={position} />
{/* Route polyline */}
{routeCoordinates.length > 1 && (
<Polyline
positions={routeCoordinates}
color="#FF6B6B"
weight={4}
opacity={0.8}
dashArray="10, 10"
/>
)}
{/* Place markers */}
{visiblePlaces.map((place) => (
<Marker
key={place.id}
position={[place.lat, place.lng]}
icon={createIcon(getCategoryEmoji(place.category), selectedPlace?.id === place.id)}
eventHandlers={{
click: () => onSelectPlace(place),
}}
>
<Popup className="custom-popup">
<div className="p-1">
<h3 className="font-bold text-gray-900">{place.name}</h3>
<p className="text-xs text-gray-500">{place.nameJp}</p>
{place.day && (
<span className="inline-block mt-1 px-2 py-0.5 bg-[#FFE8E8] text-[#FF6B6B] text-xs rounded-full">
День {place.day}
</span>
)}
</div>
</Popup>
</Marker>
))}
</MapContainer>
{/* Map controls */}
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-2">
{position && (
<button
onClick={() => {
setMapCenter([position.lat, position.lng]);
setMapZoom(15);
}}
className="w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
>
<svg className="w-5 h-5 text-[#4285F4]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
)}
</div>
{/* Route info bar */}
{showRouteForDay && (
<div className="absolute top-4 left-4 right-16 z-[1000]">
<div className="bg-white rounded-xl shadow-lg p-3 flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-gray-900">Маршрут дня {showRouteForDay}</p>
<p className="text-xs text-gray-500">{visiblePlaces.length} мест</p>
</div>
<button
onClick={onClearRoute}
className="px-3 py-1.5 bg-gray-100 text-gray-600 rounded-lg text-sm font-medium"
>
Скрыть
</button>
</div>
</div>
)}
</div>
);
}