diff --git a/CLAUDE.md b/CLAUDE.md index 3da4820..3247a08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## πŸ‡»πŸ‡³ VNGeoGuessr - Project Configuration +## VNGeoGuessr - Project Configuration A Vietnamese location-guessing game where players identify locations within Vietnamese cities using panoramic street views. @@ -13,7 +13,8 @@ A Vietnamese location-guessing game where players identify locations within Viet - **Styling**: Tailwind CSS v4 - **Component Library**: shadcn/ui - **State Management**: Zustand -- **Maps**: Google Maps JavaScript API +- **Maps**: Leaflet + OpenStreetMap +- **Street View**: Mapillary API - **Backend**: Supabase (Database, Auth, Storage, Realtime) - **Geo Calculations**: Turf.js - **Testing**: Jest + React Testing Library @@ -78,15 +79,20 @@ src/ ## πŸ“¦ Required Dependencies ```bash +# Core dependencies pnpm add @supabase/supabase-js @supabase/auth-helpers-nextjs -pnpm add zustand @turf/turf @googlemaps/js-api-loader -pnpm add -D @types/google.maps +pnpm add zustand @turf/turf +pnpm add leaflet react-leaflet +pnpm add mapillary-js + +# Development dependencies +pnpm add -D @types/leaflet ``` ## 🌍 Environment Variables ```bash -NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_api_key_here +NEXT_PUBLIC_MAPILLARY_ACCESS_TOKEN=your_mapillary_access_token NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key ``` @@ -96,14 +102,14 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key 1. **Authentication**: Login required for score saving, guest mode available with warning 2. **City Selection**: Choose from Vietnamese cities (Ho Chi Minh, Hanoi, Da Nang, etc.) 3. **Location Display**: Server sends random city location without metadata -4. **Guessing Phase**: - - View panoramic image (no location hints) +4. **Guessing Phase**: + - View Mapillary street-level imagery (no location hints) - 3-minute timer per location - - Place guess on city overview map + - Place guess on OpenStreetMap via Leaflet - Submit guess or skip to new location 5. **Scoring**: Distance-based points (0-5 scale): - 0-50m = 5 points - - 50m-100m = 4 points + - 50m-100m = 4 points - 100m-200m = 3 points - 200m-500m = 2 points - 500m-1km = 1 point @@ -125,9 +131,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key ## πŸ—ΊοΈ Maps Integration -- Panoramic street view images (no location metadata) -- City overview maps for guessing -- Distance calculation and visualization +- **Mapillary**: Street-level imagery from crowd-sourced photos +- **OpenStreetMap + Leaflet**: Interactive maps for location guessing +- **No Location Metadata**: Secure API responses without coordinates +- Distance calculation and visualization with result overlays ## πŸ’Ύ Database Schema @@ -143,8 +150,10 @@ leaderboard (user_id, city_id, best_score, total_games, last_played) - Use shadcn/ui for UI components - Responsive mobile-first design -- Lazy load Google Maps API +- Use Leaflet with OpenStreetMap tiles +- Use Mapillary Viewer for street imagery - Implement RLS policies for security +- Handle Mapillary API rate limits gracefully ## πŸ’‘ Code Examples @@ -156,9 +165,9 @@ const useGameStore = create((set) => ({ totalScore: 0, timeRemaining: 180, // 3 minutes in seconds guesses: [], - addGuess: (guess) => set((state) => ({ + addGuess: (guess) => set((state) => ({ guesses: [...state.guesses, guess], - totalScore: state.totalScore + guess.points + totalScore: state.totalScore + guess.points })), resetTimer: () => set({ timeRemaining: 180 }) })) @@ -173,12 +182,26 @@ const calculatePoints = (distanceM: number): number => { return 0 } +// Mapillary image retrieval +const getMapillaryImage = async (lat: number, lng: number) => { + const response = await fetch( + `https://graph.mapillary.com/images?access_token=${token}&fields=id,thumb_2048_url&bbox=${bbox}` + ) + return response.json() +} + +// Leaflet map initialization +const map = L.map('map').setView([lat, lng], 13) +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: 'Β© OpenStreetMap contributors' +}).addTo(map) + // Secure API response (no location hints) const getRandomLocation = async (cityId: string) => { return { id: 'location_id', - panoramaData: 'base64_image_data', // No lat/lng included + mapillary_image_id: 'image_id_here', // No lat/lng included cityBounds: { north: 10.8, south: 10.7, east: 106.8, west: 106.6 } } } -``` \ No newline at end of file +``` diff --git a/package.json b/package.json index c33fbf3..4ba5dec 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,26 @@ "lint": "next lint" }, "dependencies": { - "@googlemaps/js-api-loader": "^1.16.10", "@radix-ui/react-slot": "^1.2.3", "@supabase/auth-helpers-nextjs": "^0.10.0", "@supabase/supabase-js": "^2.53.0", "@turf/turf": "^7.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "leaflet": "^1.9.4", "lucide-react": "^0.536.0", + "mapillary-js": "^4.1.2", "next": "15.4.5", "react": "19.1.0", "react-dom": "19.1.0", + "react-leaflet": "^5.0.0", "tailwind-merge": "^3.3.1", "zustand": "^5.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", - "@types/google.maps": "^3.58.1", + "@types/leaflet": "^1.9.20", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 224ddd2..a7252f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@googlemaps/js-api-loader': - specifier: ^1.16.10 - version: 1.16.10 '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.9)(react@19.1.0) @@ -29,9 +26,15 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 lucide-react: specifier: ^0.536.0 version: 0.536.0(react@19.1.0) + mapillary-js: + specifier: ^4.1.2 + version: 4.1.2 next: specifier: 15.4.5 version: 15.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -41,6 +44,9 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + react-leaflet: + specifier: ^5.0.0 + version: 5.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -54,9 +60,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.1.11 - '@types/google.maps': - specifier: ^3.58.1 - version: 3.58.1 + '@types/leaflet': + specifier: ^1.9.20 + version: 1.9.20 '@types/node': specifier: ^20 version: 20.19.9 @@ -139,9 +145,6 @@ packages: resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@googlemaps/js-api-loader@1.16.10': - resolution: {integrity: sha512-c2erv2k7P2ilYzMmtYcMgAR21AULosQuUHJbStnrvRk2dG93k5cqptDrh9A8p+ZNlyhiqEOgHW7N9PAizdUM7Q==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -392,6 +395,13 @@ packages: '@types/react': optional: true + '@react-leaflet/core@3.0.0': + resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -874,27 +884,45 @@ packages: '@types/d3-voronoi@1.1.12': resolution: {integrity: sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==} + '@types/earcut@2.1.4': + resolution: {integrity: sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/google.maps@3.58.1': - resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/leaflet@1.9.20': + resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==} + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} + '@types/pako@1.0.7': + resolution: {integrity: sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==} + + '@types/pbf@3.0.5': + resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + '@types/polylabel@1.1.3': + resolution: {integrity: sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==} + + '@types/rbush@3.0.4': + resolution: {integrity: sha512-knSt9cCW8jj1ZSFcFeBZaX++OucmfPxxHiRwTahZfJlnQsek7O0bazTJHWD2RVj9LEoejUYF2de3/stf+QXcXw==} + '@types/react-dom@19.1.7': resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} peerDependencies: @@ -903,6 +931,12 @@ packages: '@types/react@19.1.9': resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} + '@types/three@0.134.0': + resolution: {integrity: sha512-4YB+99Rgqq27EjiYTItEoZtdjLnTh8W9LxowgpC9eWsjaQJIL4Kn/ZcUKAnW3gB/jS4hqGN8iqmid+RcUZDzpA==} + + '@types/virtual-dom@2.1.4': + resolution: {integrity: sha512-Y7L/frVydXRd16MevczslJZQu+QWsrqZlj6ytk7mST3xen0fkx7Ollw31By/89A8Wq+nfNWm/IoTR1ac/0fRhA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1151,6 +1185,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-split@0.0.1: + resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1167,6 +1204,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001731: resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} @@ -1278,6 +1318,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1292,6 +1335,9 @@ packages: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} + error@4.4.0: + resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -1444,6 +1490,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + ev-store@7.0.0: + resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1534,6 +1583,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1579,6 +1631,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1595,6 +1650,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1669,6 +1727,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1765,6 +1826,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1840,6 +1904,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@3.2.0: + resolution: {integrity: sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==} + engines: {node: '>=0.6'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1852,9 +1920,15 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mapillary-js@4.1.2: + resolution: {integrity: sha512-LYj6xbp/Y9wfBkr/prRwgJGb3+KwvzUri0yl5QEpMEH7oC1p52ErshWmPXysKefmm6JaFML4D7FJ89viwJ4T5g==} + marchingsquares@1.3.3: resolution: {integrity: sha512-gz6nNQoVK7Lkh2pZulrT4qd4347S/toG9RXH2pyzhLgkL5mLkBoqgv4EvAGXcV0ikDW72n/OQb3Xe8bGagQZCg==} + martinez-polygon-clipping@0.7.4: + resolution: {integrity: sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1867,6 +1941,9 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1906,6 +1983,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-tick@0.2.2: + resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==} + next@15.4.5: resolution: {integrity: sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -1975,6 +2055,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1990,6 +2073,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2010,6 +2097,9 @@ packages: polyclip-ts@0.16.8: resolution: {integrity: sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==} + polylabel@1.1.0: + resolution: {integrity: sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2026,9 +2116,16 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protocol-buffers-schema@3.6.0: + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2056,6 +2153,13 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-leaflet@5.0.0: + resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -2075,6 +2179,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -2097,6 +2204,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + s2-geometry@1.2.10: + resolution: {integrity: sha512-5WejfQu1XZ25ZerW8uL6xP1sM2krcOYKhI6TbfybGRf+vTQLrm3E+4n0+1lWg+MYqFjPzoe51zKhn2sBRMCt5g==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -2177,6 +2290,9 @@ packages: splaytree-ts@1.0.2: resolution: {integrity: sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==} + splaytree@0.1.4: + resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -2184,6 +2300,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -2253,10 +2372,16 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + three@0.134.0: + resolution: {integrity: sha512-LbBerg7GaSPjYtTOnu41AMp7tV6efUNR3p4Wk5NzkSsNTBuA5mDGOfwwZL1jhhVMLx9V20HolIUo0+U3AXehbg==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyqueue@1.2.3: + resolution: {integrity: sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==} + tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} @@ -2328,6 +2453,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + virtual-dom@2.1.1: + resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -2371,6 +2499,16 @@ packages: utf-8-validate: optional: true + x-is-array@0.1.0: + resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} + + x-is-string@0.1.0: + resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2466,8 +2604,6 @@ snapshots: '@eslint/core': 0.15.1 levn: 0.4.1 - '@googlemaps/js-api-loader@1.16.10': {} - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -2649,6 +2785,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.9 + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + leaflet: 1.9.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} @@ -3900,22 +4042,36 @@ snapshots: '@types/d3-voronoi@1.1.12': {} + '@types/earcut@2.1.4': {} + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} - '@types/google.maps@3.58.1': {} - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/leaflet@1.9.20': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/node@14.18.63': {} + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 + '@types/pako@1.0.7': {} + + '@types/pbf@3.0.5': {} + '@types/phoenix@1.6.6': {} + '@types/polylabel@1.1.3': {} + + '@types/rbush@3.0.4': {} + '@types/react-dom@19.1.7(@types/react@19.1.9)': dependencies: '@types/react': 19.1.9 @@ -3924,6 +4080,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/three@0.134.0': {} + + '@types/virtual-dom@2.1.4': {} + '@types/ws@8.18.1': dependencies: '@types/node': 20.19.9 @@ -4197,6 +4357,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-split@0.0.1: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4216,6 +4378,8 @@ snapshots: callsites@3.1.0: {} + camelize@1.0.1: {} + caniuse-lite@1.0.30001731: {} chalk@4.1.2: @@ -4326,6 +4490,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-walk@0.1.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4341,6 +4507,12 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + error@4.4.0: + dependencies: + camelize: 1.0.1 + string-template: 0.2.1 + xtend: 4.0.2 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -4641,6 +4813,10 @@ snapshots: esutils@2.0.3: {} + ev-store@7.0.0: + dependencies: + individual: 3.0.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -4752,6 +4928,11 @@ snapshots: dependencies: is-glob: 4.0.3 + global@4.4.0: + dependencies: + min-document: 2.19.0 + process: 0.11.10 + globals@14.0.0: {} globalthis@1.0.4: @@ -4787,6 +4968,8 @@ snapshots: dependencies: function-bind: 1.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4798,6 +4981,8 @@ snapshots: imurmurhash@0.1.4: {} + individual@3.0.0: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -4879,6 +5064,8 @@ snapshots: is-number@7.0.0: {} + is-object@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -4974,6 +5161,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leaflet@1.9.4: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5030,6 +5219,8 @@ snapshots: lodash.merge@4.6.2: {} + long@3.2.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5042,8 +5233,35 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + mapillary-js@4.1.2: + dependencies: + '@types/earcut': 2.1.4 + '@types/node': 14.18.63 + '@types/pako': 1.0.7 + '@types/pbf': 3.0.5 + '@types/polylabel': 1.1.3 + '@types/rbush': 3.0.4 + '@types/three': 0.134.0 + '@types/virtual-dom': 2.1.4 + earcut: 2.2.4 + martinez-polygon-clipping: 0.7.4 + pako: 2.1.0 + pbf: 3.3.0 + polylabel: 1.1.0 + rbush: 3.0.1 + rxjs: 7.8.2 + s2-geometry: 1.2.10 + three: 0.134.0 + virtual-dom: 2.1.1 + marchingsquares@1.3.3: {} + martinez-polygon-clipping@0.7.4: + dependencies: + robust-predicates: 2.0.4 + splaytree: 0.1.4 + tinyqueue: 1.2.3 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -5053,6 +5271,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + min-document@2.19.0: + dependencies: + dom-walk: 0.1.2 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -5079,6 +5301,8 @@ snapshots: natural-compare@1.4.0: {} + next-tick@0.2.2: {} + next@15.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.4.5 @@ -5167,6 +5391,8 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5177,6 +5403,11 @@ snapshots: path-parse@1.0.7: {} + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5194,6 +5425,10 @@ snapshots: bignumber.js: 9.3.1 splaytree-ts: 1.0.2 + polylabel@1.1.0: + dependencies: + tinyqueue: 2.0.3 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -5210,12 +5445,16 @@ snapshots: prelude-ls@1.2.1: {} + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + protocol-buffers-schema@3.6.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -5239,6 +5478,13 @@ snapshots: react-is@16.13.1: {} + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + leaflet: 1.9.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} reflect.getprototypeof@1.0.10: @@ -5265,6 +5511,10 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.0 + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -5287,6 +5537,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + s2-geometry@1.2.10: + dependencies: + long: 3.2.0 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -5411,6 +5669,8 @@ snapshots: splaytree-ts@1.0.2: {} + splaytree@0.1.4: {} + stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -5418,6 +5678,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-template@0.2.1: {} + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -5502,11 +5764,15 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + three@0.134.0: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tinyqueue@1.2.3: {} + tinyqueue@2.0.3: {} to-regex-range@5.0.1: @@ -5614,6 +5880,17 @@ snapshots: dependencies: punycode: 2.3.1 + virtual-dom@2.1.1: + dependencies: + browser-split: 0.0.1 + error: 4.4.0 + ev-store: 7.0.0 + global: 4.4.0 + is-object: 1.0.2 + next-tick: 0.2.2 + x-is-array: 0.1.0 + x-is-string: 0.1.0 + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -5670,6 +5947,12 @@ snapshots: ws@8.18.3: {} + x-is-array@0.1.0: {} + + x-is-string@0.1.0: {} + + xtend@4.0.2: {} + yallist@5.0.0: {} yocto-queue@0.1.0: {} diff --git a/src/app/game/[cityId]/page.tsx b/src/app/game/[cityId]/page.tsx index 1b2f8db..f4c48e2 100644 --- a/src/app/game/[cityId]/page.tsx +++ b/src/app/game/[cityId]/page.tsx @@ -1,15 +1,17 @@ 'use client' -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { useGameStore } from '@/stores/gameStore' import { useAuthStore } from '@/stores/authStore' import { vietnameseCities } from '@/data/cities' -import { generateRandomLocation, calculateDistance } from '@/lib/distance' +import { calculateDistance } from '@/lib/distance' import { calculatePoints, getScoreDescription } from '@/lib/scoring' -import { Loader } from '@googlemaps/js-api-loader' +import MapillaryViewer from '@/components/maps/MapillaryViewer' +import LeafletMap from '@/components/maps/LeafletMap' +import { getRandomMapillaryImage, MapillaryImage } from '@/lib/mapillary' export default function GamePage() { const params = useParams() @@ -17,10 +19,7 @@ export default function GamePage() { const cityId = params.cityId as string const [isLoading, setIsLoading] = useState(true) - const [gameMap, setGameMap] = useState(null) - const [streetView, setStreetView] = useState(null) - const [resultMap, setResultMap] = useState(null) - const [guessMarker, setGuessMarker] = useState(null) + const [mapillaryImage, setMapillaryImage] = useState(null) const [guessLocation, setGuessLocation] = useState<{lat: number, lng: number} | null>(null) const [actualLocation, setActualLocation] = useState<{lat: number, lng: number} | null>(null) const [showResults, setShowResults] = useState(false) @@ -28,10 +27,6 @@ export default function GamePage() { const [roundPoints, setRoundPoints] = useState(0) const [canGuess, setCanGuess] = useState(false) - const mapRef = useRef(null) - const streetViewRef = useRef(null) - const resultMapRef = useRef(null) - const { selectedCity, timeRemaining, @@ -59,105 +54,47 @@ export default function GamePage() { } }, [timeRemaining, showResults, decrementTimer, isAuthenticated]) - // Initialize Google Maps + // Initialize Mapillary and game useEffect(() => { if (!city) return - const initMaps = async () => { + const initGame = async () => { try { - const loader = new Loader({ - apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, - version: 'weekly', - }) - - await loader.load() + console.log('πŸ”„ Loading Mapillary image...') + setIsLoading(true) - // Generate random location in city - const location = generateRandomLocation(city.bounds) - setActualLocation(location) - - // Initialize street view - if (streetViewRef.current) { - const panorama = new google.maps.StreetViewPanorama(streetViewRef.current, { - position: location, - pov: { heading: Math.random() * 360, pitch: 0 }, - addressControl: false, - showRoadLabels: false, - disableDefaultUI: true, - clickToGo: true, - scrollwheel: false - }) - setStreetView(panorama) + // Get random Mapillary image + const image = await getRandomMapillaryImage(city.bounds) + + if (!image) { + console.error('❌ No Mapillary images found in city bounds') + setIsLoading(false) + return } - // Initialize guessing map - if (mapRef.current) { - const map = new google.maps.Map(mapRef.current, { - center: { - lat: (city.bounds.north + city.bounds.south) / 2, - lng: (city.bounds.east + city.bounds.west) / 2 - }, - zoom: 11, - disableDefaultUI: true, - zoomControl: true, - gestureHandling: 'greedy' - }) - - // Add click listener for guessing - map.addListener('click', (event: google.maps.MapMouseEvent) => { - if (event.latLng && !showResults) { - placeGuessMarker(event.latLng) - } - }) - - setGameMap(map) - } + console.log('βœ… Mapillary image loaded:', image.id) + setMapillaryImage(image) + + // Set actual location from Mapillary image coordinates + const [lng, lat] = image.computed_geometry.coordinates + setActualLocation({ lat, lng }) + console.log('πŸ“ Actual location:', { lat, lng }) setIsLoading(false) setIsPlaying(true) resetTimer() } catch (error) { - console.error('Failed to load Google Maps:', error) + console.error('❌ Failed to load game:', error) setIsLoading(false) } } - initMaps() + initGame() }, [city, resetTimer, setIsPlaying]) - const placeGuessMarker = (latLng: google.maps.LatLng) => { - if (!gameMap) return - - // Remove existing marker - if (guessMarker) { - guessMarker.setMap(null) - } - - // Place new marker - const marker = new google.maps.Marker({ - position: latLng, - map: gameMap, - draggable: true, - title: 'Your Guess' - }) - - // Update guess location when marker is dragged - marker.addListener('dragend', () => { - const position = marker.getPosition() - if (position) { - setGuessLocation({ - lat: position.lat(), - lng: position.lng() - }) - } - }) - - setGuessMarker(marker) - setGuessLocation({ - lat: latLng.lat(), - lng: latLng.lng() - }) + const handleGuessPlaced = (lat: number, lng: number) => { + setGuessLocation({ lat, lng }) setCanGuess(true) } @@ -180,7 +117,7 @@ export default function GamePage() { addGuess({ id: `guess-${Date.now()}`, sessionId: 'temp-session', - locationId: 'temp-location', + locationId: mapillaryImage?.id || 'temp-location', guessLat: guessLocation.lat, guessLng: guessLocation.lng, distanceM: distance, @@ -188,7 +125,6 @@ export default function GamePage() { timeTaken: 180 - timeRemaining }) - showResultsMap() setShowResults(true) setCanGuess(false) } @@ -200,85 +136,31 @@ export default function GamePage() { // No guess made - 0 points setRoundDistance(0) setRoundPoints(0) - showResultsMap() setShowResults(true) } } - const showResultsMap = () => { - if (!resultMapRef.current || !actualLocation) return - - const map = new google.maps.Map(resultMapRef.current, { - center: actualLocation, - zoom: 13, - disableDefaultUI: true, - zoomControl: true - }) - - // Actual location marker (green) - new google.maps.Marker({ - position: actualLocation, - map: map, - title: 'Actual Location', - icon: { - url: 'http://maps.google.com/mapfiles/ms/icons/green-dot.png' - } - }) - - // Guess location marker (red) if guess was made - if (guessLocation) { - new google.maps.Marker({ - position: guessLocation, - map: map, - title: 'Your Guess', - icon: { - url: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png' - } - }) - - // Draw line between guess and actual - const line = new google.maps.Polyline({ - path: [actualLocation, guessLocation], - strokeColor: '#FF0000', - strokeOpacity: 0.8, - strokeWeight: 2, - geodesic: true - }) - line.setMap(map) - - // Adjust map bounds to show both markers - const bounds = new google.maps.LatLngBounds() - bounds.extend(actualLocation) - bounds.extend(guessLocation) - map.fitBounds(bounds) - } - - setResultMap(map) - } - - const handleNextRound = () => { + const handleNextRound = async () => { // Reset for next round setShowResults(false) setGuessLocation(null) setActualLocation(null) + setMapillaryImage(null) setRoundDistance(0) setRoundPoints(0) setCanGuess(false) - - if (guessMarker) { - guessMarker.setMap(null) - setGuessMarker(null) - } + setIsLoading(true) - // Generate new location and reinitialize - if (city && streetView && gameMap) { - const location = generateRandomLocation(city.bounds) - setActualLocation(location) - - streetView.setPosition(location) - streetView.setPov({ heading: Math.random() * 360, pitch: 0 }) - - resetTimer() + if (city) { + // Get new Mapillary image + const image = await getRandomMapillaryImage(city.bounds) + if (image) { + setMapillaryImage(image) + const [lng, lat] = image.computed_geometry.coordinates + setActualLocation({ lat, lng }) + resetTimer() + } + setIsLoading(false) } } @@ -315,6 +197,7 @@ export default function GamePage() {

Loading {city.name}...

+

Fetching Mapillary images...

@@ -341,21 +224,20 @@ export default function GamePage() { {/* Game Layout */}
- {/* Street View */} + {/* Mapillary Street View */} Street View -
- {/* Guessing Map or Results */} + {/* Leaflet Map or Results */} @@ -363,13 +245,23 @@ export default function GamePage() { - {showResults ? ( -
-
+
+ +
+ + {showResults ? (

{getScoreDescription(roundPoints)} @@ -381,14 +273,7 @@ export default function GamePage() { Next Location

-
- ) : ( -
-
+ ) : (

Click on the map to place your guess @@ -410,8 +295,8 @@ export default function GamePage() {

-
- )} + )} +
diff --git a/src/app/page.tsx b/src/app/page.tsx index 2f775eb..4b85846 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,7 +14,7 @@ export default function Home() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showGuestWarning, setShowGuestWarning] = useState(false) - + const router = useRouter() const { user, isAuthenticated, login, playAsGuest, isLoading } = useAuthStore() const { setSelectedCity } = useGameStore() @@ -46,7 +46,7 @@ export default function Home() {
-

πŸ‡»πŸ‡³ VNGeoGuessr

+

VNGeoGuessr

Explore Vietnam through Street View

@@ -55,7 +55,7 @@ export default function Home() { ⚠️ Guest Mode Warning - Playing as a guest means your scores will not be saved to the leaderboard. + Playing as a guest means your scores will not be saved to the leaderboard. Are you sure you want to continue? @@ -63,9 +63,9 @@ export default function Home() { -
-