diff --git a/docs/documentation/docs/assets/WorldMap.png b/docs/documentation/docs/assets/WorldMap.png new file mode 100644 index 000000000..ff5c74a66 Binary files /dev/null and b/docs/documentation/docs/assets/WorldMap.png differ diff --git a/docs/documentation/docs/controls/WorldMap.md b/docs/documentation/docs/controls/WorldMap.md new file mode 100644 index 000000000..c3253f861 --- /dev/null +++ b/docs/documentation/docs/controls/WorldMap.md @@ -0,0 +1,423 @@ +# WorldMapControl Documentation + +A React component for displaying interactive world maps with markers, search functionality, and customizable styling using MapLibre GL JS. + +This use Mapitiler API for map tiles and supports custom styles. +By default, it uses a free demo map without requiring an API key, but if you want to use MapTiler or a custom style, you can provide an API key and style URL. +To Get the API key, you can register on [MapTiler](https://www.maptiler.com/) and create a Account to get your API key. + +## Table of Contents + +- [Installation](#installation) +- [Basic Usage](#basic-usage) +- [Properties](#properties) +- [Data Structure](#data-structure) +- [Map Configuration](#map-configuration) +- [Search Feature](#search-feature) +- [Marker Customization](#marker-customization) +- [Examples](#examples) + +## Installation + +```bash +npm install @pnp/spfx-controls-react +``` + +## Example of WorldMapControl +![WorldMapControl Example](../assets/WorldMap.png) + +## Basic Usage + +```tsx +import React from 'react'; +import { MaplibreWorldMap } from '@pnp/spfx-controls-react/lib/WorldMapControl'; +import { IData } from '@pnp/spfx-controls-react/lib/worldMap'; + +const MyMapComponent: React.FC = () => { + const locations: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/nyc.jpg', + link: 'https://example.com/nyc', + coordinates: [-74.006, 40.7128] + }, + { + id: '2', + name: 'London', + imageUrl: 'https://example.com/london.jpg', + link: 'https://example.com/london', + coordinates: [-0.1276, 51.5074] + } + ]; + + const handleMarkerClick = (location: IData) => { + console.log('Clicked:', location.name); + // Navigate to location.link or show details + }; + + return ( + + ); +}; +``` + +## Properties + +### Core Properties + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `data` | `IData[]` | ✅ | - | Array of location data to display | +| `onClick` | `(item: IData) => void` | ❌ | - | Callback when marker is clicked | +| `title` | `string \| ReactNode` | ❌ | `'World Map'` | Map title | +| `style` | `CSSProperties` | ❌ | - | Custom styles for map container | +| `className` | `string` | ❌ | - | CSS class for root container | +| `theme` | `Theme` | ❌ | - | Fluent UI theme object | + +### Map Configuration + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `mapKey` | `string` | ❌ | - | MapTiler API key | +| `mapStyleUrl` | `string` | ❌ | - | Custom map style URL | +| `fitPadding` | `number` | ❌ | `20` | Padding when fitting to markers | + +### Search Configuration + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `search.enabled` | `boolean` | ❌ | `true` | Enable search feature | +| `search.placeholder` | `string` | ❌ | `'Search locations...'` | Search input placeholder | +| `search.searchField` | `keyof IData \| function` | ❌ | `'name'` | Field to search or custom function | +| `search.zoomLevel` | `number` | ❌ | `8` | Zoom level for search results | +| `search.position` | `object` | ❌ | `{top: '10px', left: '10px'}` | Search overlay position | +| `search.onSearchChange` | `function` | ❌ | - | Callback when search changes | + +### Marker Configuration + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `marker.markerClassName` | `string` | ❌ | - | CSS class for markers | +| `marker.markerStyle` | `CSSProperties` | ❌ | - | Custom marker styles | +| `marker.imageSize` | `number` | ❌ | `40` | Marker image size (px) | +| `marker.renderToolTip` | `function` | ❌ | - | Custom tooltip renderer | +| `marker.tooltipClassName` | `string` | ❌ | - | CSS class for tooltips | +| `marker.tooltipStyle` | `CSSProperties` | ❌ | - | Custom tooltip styles | + +## Data Structure + +Each location item must implement the `IData` interface: + +```tsx +interface IData { + id: string; // Unique identifier + name: string; // Display name (used for search) + imageUrl: string; // Image URL for marker/tooltip + link: string; // Associated URL/link + coordinates: [number, number]; // [longitude, latitude] +} +``` + +### Example Data + +```tsx +const sampleData: IData[] = [ + { + id: 'paris-001', + name: 'Paris, France', + imageUrl: 'https://example.com/images/paris.jpg', + link: 'https://example.com/locations/paris', + coordinates: [2.3522, 48.8566] // [longitude, latitude] + }, + { + id: 'tokyo-001', + name: 'Tokyo, Japan', + imageUrl: 'https://example.com/images/tokyo.jpg', + link: '/locations/tokyo', + coordinates: [139.6917, 35.6895] + } +]; +``` + +## Map Configuration + +### Using Demo Map (Free) + +```tsx + +``` + +### Using MapTiler API Key + +```tsx + +``` + +### Custom Map Style + +```tsx + +``` + +### Map Style Priority + +1. **mapKey + mapStyleUrl**: Uses the style URL with API key +2. **mapKey only**: Uses MapTiler streets style +3. **mapStyleUrl only**: Uses URL as-is +4. **Neither**: Uses free demo map + +## Search Feature + +### Basic Search + +```tsx + +``` + +### Custom Search Field + +```tsx + +``` + +### Custom Search Function + +```tsx + `${item.name} ${item.link}`, // Search multiple fields + onSearchChange: (term, results) => { + console.log(`Found ${results.length} results for "${term}"`); + } + }} +/> +``` + +### Search Positioning + +```tsx + +``` + +## Marker Customization + +### Basic Marker Styling + +```tsx + +``` + +### Custom Tooltips + +```tsx + ( +
+

{item.name}

+ {item.name} +

Click to visit: Learn more

+
+ ), + tooltipStyle: { + backgroundColor: 'white', + border: '1px solid #ccc', + borderRadius: '8px', + padding: '12px', + maxWidth: '200px' + } + }} +/> +``` + +## Examples + +### Complete Example with All Features + +```tsx +import React from 'react'; +import { MaplibreWorldMap } from '@pnp/spfx-controls-react/lib/WorldMapControl'; +import { IData } from '@pnp/spfx-controls-react/lib/worldMap'; + +const AdvancedMapExample: React.FC = () => { + const locations: IData[] = [ + { + id: 'nyc', + name: 'New York City', + imageUrl: 'https://example.com/nyc.jpg', + link: 'https://example.com/nyc', + coordinates: [-74.006, 40.7128] + }, + // ... more locations + ]; + + const handleLocationClick = (location: IData) => { + window.open(location.link, '_blank'); + }; + + const handleSearchChange = (term: string, results: IData[]) => { + console.log(`Search: "${term}" - ${results.length} results`); + }; + + return ( + `${item.name} ${item.id}` + }} + marker={{ + imageSize: 45, + markerStyle: { + borderRadius: '50%', + border: '3px solid #0078d4', + boxShadow: '0 2px 8px rgba(0,0,0,0.3)' + }, + renderToolTip: (item) => ( +
+

{item.name}

+ {item.name} +

+ Click marker to visit location page +

+
+ ), + tooltipStyle: { + backgroundColor: 'white', + border: '1px solid #ccc', + borderRadius: '8px', + padding: '12px', + maxWidth: '160px', + textAlign: 'center' + } + }} + /> + ); +}; + +export default AdvancedMapExample; +``` + +### Minimal Example + +```tsx + alert(`Clicked: ${location.name}`)} +/> +``` + +## TypeScript Support + +The component is fully typed with TypeScript. Import the interfaces for type safety: + +```tsx +import { IMaplibreWorldMapProps, IData } from '@pnp/spfx-controls-react/lib/worldMap'; + +const MyComponent: React.FC<{locations: IData[]}> = ({ locations }) => { + const mapProps: IMaplibreWorldMapProps = { + data: locations, + onClick: (item) => console.log(item.name), + // ... other props with full type checking + }; + + return ; +}; +``` + +## Browser Support + +- Modern browsers with ES6+ support +- WebGL support required for map rendering +- Mobile browsers supported + +## Performance Tips + +1. **Limit data size**: For large datasets (>100 markers), consider clustering or pagination +2. **Optimize images**: Use appropriate image sizes for markers +3. **API key**: Use your own MapTiler API key for production to avoid rate limits +4. **Lazy loading**: Load the component only when needed to reduce initial bundle size + +## Troubleshooting + +### Common Issues + +1. **Map not loading**: Check if `mapKey` is valid or use demo map +2. **Markers not appearing**: Verify `coordinates` format is `[longitude, latitude]` +3. **Search not working**: Ensure `searchField` matches available data properties +4. **Performance issues**: Reduce number of markers or optimize marker images + +### Getting Help + +- Check the browser console for error messages +- Verify all required properties are provided +- Ensure coordinate format is correct (longitude first, then latitude) +- Test with demo data to isolate issues diff --git a/package-lock.json b/package-lock.json index b45bfe0dc..c79048804 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,13 +63,16 @@ "he": "^1.2.0", "jotai": "^2.4.2", "lodash": "4.17.21", + "maplibre-gl": "^5.6.1", "markdown-it": "^14.1.0", "monaco-editor": "^0.29.1", "nano-css": "^5.3.4", + "pmtiles": "^4.3.0", "react": "17.0.1", "react-accessible-accordion": "^5.0.0", "react-dom": "17.0.1", "react-dropzone": "^14.2.3", + "react-map-gl": "^8.0.4", "react-mentions": "^4.3.0", "react-quill": "2.0.0", "regexify-string": "^1.0.16", @@ -90,6 +93,7 @@ "@types/he": "^1.1.2", "@types/jest": "25.2.3", "@types/lodash": "4.14.202", + "@types/maplibre-gl": "^1.13.2", "@types/quill": "^1.3.10", "@types/react": "17.0.45", "@types/react-dom": "17.0.17", @@ -5169,6 +5173,94 @@ "@lit-labs/ssr-dom-shim": "^1.0.0" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "19.3.3", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", + "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^3.0.0", + "minimist": "^1.2.8", + "rw": "^1.3.3", + "sort-object": "^3.0.3" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.15.2", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.15.2.tgz", @@ -14164,6 +14256,21 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/glob-stream": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-8.0.2.tgz", @@ -14599,6 +14706,33 @@ "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==" }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/maplibre-gl": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/maplibre-gl/-/maplibre-gl-1.13.2.tgz", + "integrity": "sha512-IC1RBMhKXpGDpiFsEwt17c/hbff0GCS/VmzqmrY6G+kyy2wfv2e7BoSQRAfqrvhBQPCoO8yc0SNCi5HkmCcVqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -14692,6 +14826,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, "node_modules/@types/picomatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", @@ -14862,6 +15002,15 @@ "@types/node": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/tapable": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", @@ -15660,6 +15809,41 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vis.gl/react-mapbox": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.0.4.tgz", + "integrity": "sha512-NFk0vsWcNzSs0YCsVdt2100Zli9QWR+pje8DacpLkkGEAXFaJsFtI1oKD0Hatiate4/iAIW39SQHhgfhbeEPfQ==", + "license": "MIT", + "peerDependencies": { + "mapbox-gl": ">=3.5.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "mapbox-gl": { + "optional": true + } + } + }, + "node_modules/@vis.gl/react-maplibre": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.0.4.tgz", + "integrity": "sha512-HwZyfLjEu+y1mUFvwDAkVxinGm8fEegaWN+O8np/WZ2Sqe5Lv6OXFpV6GWz9LOEvBYMbGuGk1FQdejo+4HCJ5w==", + "license": "MIT", + "dependencies": { + "@maplibre/maplibre-gl-style-spec": "^19.2.1" + }, + "peerDependencies": { + "maplibre-gl": ">=4.0.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "maplibre-gl": { + "optional": true + } + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.12", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", @@ -16822,7 +17006,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -17833,6 +18016,25 @@ "node": ">= 0.8" } }, + "node_modules/bytewise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", + "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", + "license": "MIT", + "dependencies": { + "bytewise-core": "^1.2.2", + "typewise": "^1.0.3" + } + }, + "node_modules/bytewise-core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", + "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", + "license": "MIT", + "dependencies": { + "typewise-core": "^1.2" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -21858,6 +22060,12 @@ "node": ">=0.10.0" } }, + "node_modules/earcut": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "license": "ISC" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -23990,6 +24198,12 @@ "fast-loops": "^1.0.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", @@ -24990,6 +25204,12 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -25108,7 +25328,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -25140,6 +25359,12 @@ "node": ">= 0.10.0" } }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", @@ -27009,7 +27234,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -27518,7 +27742,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "dependencies": { "is-plain-object": "^2.0.4" }, @@ -27530,7 +27753,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -32761,6 +32983,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-pretty-compact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", + "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -33193,6 +33421,12 @@ "node": ">=10" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyborg": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz", @@ -33889,6 +34123,129 @@ "node": ">=0.10.0" } }, + "node_modules/maplibre-gl": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.6.1.tgz", + "integrity": "sha512-TTSfoTaF7RqKUR9wR5qDxCHH2J1XfZ1E85luiLOx0h8r50T/LnwAwwfV0WVNh9o8dA7rwt57Ucivf1emyeukXg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^23.3.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.1", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz", + "integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/maplibre-gl/node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/maplibre-gl/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/maplibre-gl/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/maplibre-gl/node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/maplibre-gl/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maplibre-gl/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -34560,6 +34917,12 @@ "node": ">=8" } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/mute-stdout": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", @@ -36233,6 +36596,19 @@ "node": "*" } }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -36427,6 +36803,15 @@ "node": ">=0.10.0" } }, + "node_modules/pmtiles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.3.0.tgz", + "integrity": "sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA==", + "license": "BSD-3-Clause", + "dependencies": { + "fflate": "^0.8.2" + } + }, "node_modules/pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -37145,6 +37530,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==", + "license": "ISC" + }, "node_modules/preferred-pm": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.4.tgz", @@ -37355,6 +37746,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -37504,6 +37901,12 @@ "node": ">=8" } }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/quill": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", @@ -37786,6 +38189,30 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-map-gl": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.0.4.tgz", + "integrity": "sha512-SHdpvFIvswsZBg6BCPcwY/nbKuCo3sJM1Cj7Sd+gA3gFRFOixD+KtZ2XSuUWq2WySL2emYEXEgrLZoXsV4Ut4Q==", + "license": "MIT", + "dependencies": { + "@vis.gl/react-mapbox": "8.0.4", + "@vis.gl/react-maplibre": "8.0.4" + }, + "peerDependencies": { + "mapbox-gl": ">=1.13.0", + "maplibre-gl": ">=1.13.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "mapbox-gl": { + "optional": true + }, + "maplibre-gl": { + "optional": true + } + } + }, "node_modules/react-mentions": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.4.10.tgz", @@ -38836,6 +39263,15 @@ "node": ">= 0.10" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -39035,6 +39471,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -39946,7 +40388,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -39961,7 +40402,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, "dependencies": { "is-extendable": "^0.1.0" }, @@ -39973,7 +40413,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -39982,7 +40421,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -40475,6 +40913,15 @@ "node": ">=4" } }, + "node_modules/sort-asc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", + "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sort-css-media-queries": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-1.5.4.tgz", @@ -40483,6 +40930,15 @@ "node": ">= 6.3.0" } }, + "node_modules/sort-desc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", + "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sort-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", @@ -40507,6 +40963,32 @@ "node": ">=8" } }, + "node_modules/sort-object": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", + "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", + "license": "MIT", + "dependencies": { + "bytewise": "^1.1.0", + "get-value": "^2.0.2", + "is-extendable": "^0.1.1", + "sort-asc": "^0.2.0", + "sort-desc": "^0.2.0", + "union-value": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-object/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -41058,7 +41540,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, "dependencies": { "extend-shallow": "^3.0.0" }, @@ -41070,7 +41551,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -41650,6 +42130,15 @@ "node": ">=0.8" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -42312,6 +42801,12 @@ "ms": "^2.1.1" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -43041,6 +43536,21 @@ "node": ">=14.17" } }, + "node_modules/typewise": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", + "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", + "license": "MIT", + "dependencies": { + "typewise-core": "^1.2.0" + } + }, + "node_modules/typewise-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", + "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==", + "license": "MIT" + }, "node_modules/ua-parser-js": { "version": "0.7.39", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz", @@ -43194,7 +43704,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -43209,7 +43718,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -43218,7 +43726,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -43690,6 +44197,17 @@ "node": ">=0.10.0" } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 8f1f7b1be..e1f64a319 100644 --- a/package.json +++ b/package.json @@ -76,13 +76,16 @@ "he": "^1.2.0", "jotai": "^2.4.2", "lodash": "4.17.21", + "maplibre-gl": "^5.6.1", "markdown-it": "^14.1.0", "monaco-editor": "^0.29.1", "nano-css": "^5.3.4", + "pmtiles": "^4.3.0", "react": "17.0.1", "react-accessible-accordion": "^5.0.0", "react-dom": "17.0.1", "react-dropzone": "^14.2.3", + "react-map-gl": "^8.0.4", "react-mentions": "^4.3.0", "react-quill": "2.0.0", "regexify-string": "^1.0.16", @@ -103,6 +106,7 @@ "@types/he": "^1.1.2", "@types/jest": "25.2.3", "@types/lodash": "4.14.202", + "@types/maplibre-gl": "^1.13.2", "@types/quill": "^1.3.10", "@types/react": "17.0.45", "@types/react-dom": "17.0.17", diff --git a/src/WorldMap.ts b/src/WorldMap.ts new file mode 100644 index 000000000..77282d866 --- /dev/null +++ b/src/WorldMap.ts @@ -0,0 +1 @@ +export * from './controls/worldMap/index'; diff --git a/src/controls/worldMap/IData.ts b/src/controls/worldMap/IData.ts new file mode 100644 index 000000000..a3765b5e0 --- /dev/null +++ b/src/controls/worldMap/IData.ts @@ -0,0 +1,40 @@ + +/** + * Data structure for map location items. + * Represents a location point with metadata for display on the world map. + */ +export interface IData { + /** + * Unique identifier for the location. + * @example "nyc-001" or "location-1" + */ + id: string; + + /** + * Display name of the location. + * Used for search functionality and marker tooltips. + * @example "New York City" or "Eiffel Tower" + */ + name: string; + + /** + * URL to an image representing this location. + * Used for marker display or in tooltips. + * @example "https://example.com/nyc-skyline.jpg" + */ + imageUrl: string; + + /** + * URL link associated with this location. + * Can be used for navigation when marker is clicked. + * @example "https://example.com/locations/new-york" or "/details/nyc" + */ + link: string; + + /** + * Geographic coordinates as [longitude, latitude] tuple. + * Used to position the marker on the map. + * @example [-74.006, 40.7128] // New York City coordinates + */ + coordinates: [number, number]; +} diff --git a/src/controls/worldMap/IMaplibreWorldMapProps.tsx b/src/controls/worldMap/IMaplibreWorldMapProps.tsx new file mode 100644 index 000000000..19cf96616 --- /dev/null +++ b/src/controls/worldMap/IMaplibreWorldMapProps.tsx @@ -0,0 +1,241 @@ +import { IData } from "./IData"; +import React from "react"; +import { Theme } from "@fluentui/react-components"; + +/** Props for the world map component. */ +export interface IMaplibreWorldMapProps { + /** + * Array of location data to display on the map. Each item must include coordinates as [longitude, latitude]. + * @example + * ```tsx + * const data = [ + * { id: '1', name: 'New York', coordinates: [-74.006, 40.7128], imageUrl: '...', link: '...' } + * ]; + * ``` + */ + data: IData[]; + + /** + * Callback function triggered when a marker is clicked. + * @param c - The data item associated with the clicked marker + * @example + * ```tsx + * onClick={(location) => console.log('Clicked:', location.name)} + * ``` + */ + onClick?: (c: IData) => void; + + /** + * Custom map style URL. If provided with mapKey, the key will be automatically appended. + * If provided without mapKey, the URL will be used as-is. + * @example + * ```tsx + * mapStyleUrl="https://api.maptiler.com/maps/satellite/style.json" + * ``` + */ + mapStyleUrl?: string; + + /** + * MapTiler API key for accessing premium map styles. + * If provided alone, uses MapTiler streets style by default. + * If not provided, falls back to free demo map. + * @example + * ```tsx + * mapKey="your-maptiler-api-key-here" + * ``` + */ + mapKey?: string; + + /** + * Custom CSS styles for the map container. + * @example + * ```tsx + * style={{ width: '100%', height: '500px', border: '1px solid #ccc' }} + * ``` + */ + style?: React.CSSProperties; + + /** + * Padding (in pixels) around the map when fitting bounds to show all markers. + * @default 20 + * @example + * ```tsx + * fitPadding={50} // Adds 50px padding around markers when auto-fitting + * ``` + */ + fitPadding?: number; + + /** + * Title displayed above the map. Can be a string or React element. + * @default 'World Map' + * @example + * ```tsx + * title="My Custom Map" + * // or + * title={

Interactive Location Map

} + * ``` + */ + title?: string | React.ReactNode; + + /** + * Description text displayed below the title (not currently implemented in UI). + * @example + * ```tsx + * description="Click on markers to view location details" + * ``` + */ + description?: string | React.ReactNode; + + /** + * CSS class name applied to the root container. + * @example + * ```tsx + * className="my-custom-map-class" + * ``` + */ + className?: string; + + /** + * Configuration options for map markers appearance and behavior. + */ + marker?: { + /** + * CSS class name applied to marker elements. + * @example "custom-marker-style" + */ + markerClassName?: string; + + /** + * Custom CSS styles applied to marker elements. + * @example {{ backgroundColor: 'red', borderRadius: '50%' }} + */ + markerStyle?: React.CSSProperties; + + /** + * Size of marker images in pixels. + * @default 40 + * @example 60 + */ + imageSize?: number; + + /** + * Custom function to render tooltip content for markers. + * @param c - The data item for the marker + * @returns React element to display in tooltip + * @example + * ```tsx + * renderToolTip={(item) =>
{item.name}
{item.description}
} + * ``` + */ + renderToolTip?: (c: IData) => React.ReactNode; + + /** + * CSS class name applied to tooltip elements. + * @example "custom-tooltip-style" + */ + tooltipClassName?: string; + + /** + * Custom CSS styles applied to tooltip elements. + * @example {{ backgroundColor: 'black', color: 'white', padding: '8px' }} + */ + tooltipStyle?: React.CSSProperties; + } + + /** + * Configuration options for the search functionality. + */ + search?: { + /** + * Enable or disable the search feature. + * @default true + * @example + * ```tsx + * search={{ enabled: false }} // Disables search + * ``` + */ + enabled?: boolean; + + /** + * Placeholder text displayed in the search input. + * @default "Search locations..." + * @example + * ```tsx + * search={{ placeholder: "Find a city or landmark..." }} + * ``` + */ + placeholder?: string; + + /** + * Callback function triggered when search term changes or results are filtered. + * @param searchTerm - The current search term + * @param filteredData - Array of data items matching the search + * @example + * ```tsx + * onSearchChange={(term, results) => { + * console.log(`Search: "${term}" found ${results.length} results`); + * }} + * ``` + */ + onSearchChange?: (searchTerm: string, filteredData: IData[]) => void; + + /** + * Field to search on or a custom function to extract the search term from data items. + * @default "name" + * @example + * ```tsx + * // Search by specific field + * searchField: "id" + * + * // Custom search function + * searchField: (item) => `${item.name} ${item.description}` + * ``` + */ + searchField?: keyof IData | ((item: IData) => string); + + /** + * Zoom level to use when focusing on search results. + * @default 8 + * @example + * ```tsx + * search={{ zoomLevel: 12 }} // Closer zoom for search results + * ``` + */ + zoomLevel?: number; + + /** + * Position of the search overlay on the map. + * @default { top: '10px', left: '10px' } + * @example + * ```tsx + * // Top-right position + * search={{ position: { top: '10px', right: '10px' } }} + * + * // Bottom-left position + * search={{ position: { bottom: '20px', left: '20px' } }} + * ``` + */ + position?: { + /** Distance from the top edge of the map */ + top?: string; + /** Distance from the left edge of the map */ + left?: string; + /** Distance from the right edge of the map */ + right?: string; + /** Distance from the bottom edge of the map */ + bottom?: string; + }; + } + + /** + * Fluent UI theme object for consistent styling. + * @example + * ```tsx + * import { useTheme } from '@fluentui/react-components'; + * + * const theme = useTheme(); + * + * ``` + */ + theme?: Theme +} diff --git a/src/controls/worldMap/IMarker.ts b/src/controls/worldMap/IMarker.ts new file mode 100644 index 000000000..11afe8c3e --- /dev/null +++ b/src/controls/worldMap/IMarker.ts @@ -0,0 +1,10 @@ +import { IData } from "./IData"; +import React from "react"; + +export interface IMarker { + markerClassName?: string; + markerStyle?: React.CSSProperties; + renderToolTip?: (c: IData) => React.ReactNode; + tooltipClassName?: string; + tooltipStyle?: React.CSSProperties; +} diff --git a/src/controls/worldMap/IMarkerProps.tsx b/src/controls/worldMap/IMarkerProps.tsx new file mode 100644 index 000000000..bc947e995 --- /dev/null +++ b/src/controls/worldMap/IMarkerProps.tsx @@ -0,0 +1,7 @@ +import { IData } from './IData'; +import { IMarker } from './IMarker'; + +export interface IMarkerProps extends IMarker { + data: IData; + onClick?: (data: IData) => void; +} diff --git a/src/controls/worldMap/IWorldMapProps.ts b/src/controls/worldMap/IWorldMapProps.ts new file mode 100644 index 000000000..43a5a1524 --- /dev/null +++ b/src/controls/worldMap/IWorldMapProps.ts @@ -0,0 +1,13 @@ +import { Theme } from "@fluentui/react-components"; + +export interface IWorldMapProps { + description: string; + isDarkTheme: boolean; + hasTeamsContext: boolean; + title: string; + theme?: Theme + styles?: React.CSSProperties; + className?: string; + mapStyleUrl?: string; + fitPadding?: number; +} diff --git a/src/controls/worldMap/MapNavigation.tsx b/src/controls/worldMap/MapNavigation.tsx new file mode 100644 index 000000000..71442472a --- /dev/null +++ b/src/controls/worldMap/MapNavigation.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; + +import { + ArrowReset24Regular, + ZoomIn24Regular, + ZoomOut24Regular, +} from '@fluentui/react-icons'; +import { + Button, + Tooltip, + shorthands, +} from '@fluentui/react-components'; + +import { MapRef } from 'react-map-gl/maplibre'; +import { css } from '@emotion/css'; +import strings from 'ControlStrings'; + +export interface MapNavigationProps { + mapRef: React.RefObject; + initialViewState?: { longitude: number; latitude: number; zoom: number }; + vertical?: boolean; +} + +const navStyle = css` + position: absolute; + top: 16px; + right: 16px; + z-index: 2; + background: tokens.colorNeutralBackground1; + border-radius: 8px; + box-shadow: tokens.shadow4; + padding: 4px; + display: flex; + gap: 8px; +`; + +const navStyleVertical = css` + flex-direction: column; +`; + +const buttonStyle = css` + min-width: 32px; + min-height: 32px; + ${shorthands.padding('4px')} +`; + +export const MapNavigation: React.FC = ({ + mapRef, + initialViewState = { longitude: 0, latitude: 20, zoom: 1 }, + vertical = true, +}) => { + const handleZoomIn = React.useCallback((): void => { + mapRef.current?.getMap().zoomIn(); + }, [mapRef]); + const handleZoomOut = React.useCallback((): void => { + mapRef.current?.getMap().zoomOut(); + }, [mapRef]); + const handleReset = React.useCallback((): void => { + mapRef.current?.flyTo({ + center: [initialViewState.longitude, initialViewState.latitude], + zoom: initialViewState.zoom, + essential: true, + }); + }, [mapRef, initialViewState]); + + return ( +
+ +
+ ); +}; + +export default MapNavigation; diff --git a/src/controls/worldMap/Marker.tsx b/src/controls/worldMap/Marker.tsx new file mode 100644 index 000000000..5b062b8c7 --- /dev/null +++ b/src/controls/worldMap/Marker.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; + +import { Tooltip, tokens } from '@fluentui/react-components'; + +import { IMarkerProps } from './IMarkerProps'; +import { Marker as MapMarker } from 'react-map-gl/maplibre'; +import TooltipContent from './TooltipContent'; +import { css } from '@emotion/css'; +import strings from 'ControlStrings'; + +const useStyles = (): { flag: string; tooltipContent: string } => { + return { + flag: css` + width: 22px; + height: 10px; + border-radius: 4px; + box-shadow: '${tokens.shadow4}; + border: 1px solid ${tokens.colorNeutralStroke2}; + cursor: pointer; + display: block; + margin: 0 auto; + `, + tooltipContent: css` + background-color: ${tokens.colorBrandBackground2}; + `, + }; +}; + +export const Marker: React.FC = ({ + data, + onClick, + markerClassName, + markerStyle, + + renderToolTip, + tooltipClassName, + tooltipStyle, +}) => { + const styles = useStyles(); + + return ( + onClick && onClick(data)} + className={markerClassName ?? styles.flag} + style={markerStyle ?? undefined} + > + , + style: { ...tooltipStyle }, + className: tooltipClassName ?? styles.tooltipContent, + }} + relationship="label" + > + {`${data.name} (e.currentTarget.style.transform = 'scale(1.1)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = '')} + style={{ cursor: 'pointer' }} + /> + + + ); +}; +export default Marker; diff --git a/src/controls/worldMap/TooltipContent.tsx b/src/controls/worldMap/TooltipContent.tsx new file mode 100644 index 000000000..e7e40b585 --- /dev/null +++ b/src/controls/worldMap/TooltipContent.tsx @@ -0,0 +1,74 @@ +import * as React from "react"; + +import { + Text, + tokens, +} from "@fluentui/react-components"; + +import { IData } from "./IData"; +import { css } from "@emotion/css"; +import strings from "ControlStrings"; + +export interface CountryTooltipContentProps { + data: IData; +} + +const stackStyles = css` + min-width: 160px; + + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; + background-color: ${tokens.colorBrandBackground2}; + padding: 0; + min-width: 100px; +`; + +const rowStyles = css` + display: flex; + align-items: center; + gap: 8px; +`; + +const imageStyles = css` + width: 32px; + height: 20px; + display: block; + border-radius: 4px; + box-shadow: '${tokens.shadow4}; +`; + +const titleStyles = css` + font-weight: 600; + font-size: 1rem; +`; + +const subTitleStyles = css` + color: '${tokens.colorNeutralForeground2}; + font-size: 0.92rem; +`; + +export const TooltipContent: React.FC = ({ + data, +}) => { + return ( +
+
+ {`${data.name} + {data.name} +
+
+ {strings.worldMapCoord} + {data.coordinates[1].toFixed(2)}{strings.worldMapN} + {data.coordinates[0].toFixed(2)}{strings.worldMapE} +
+
+ ); +}; + +export default TooltipContent; diff --git a/src/controls/worldMap/WorldMap.tsx b/src/controls/worldMap/WorldMap.tsx new file mode 100644 index 000000000..a8e92b85f --- /dev/null +++ b/src/controls/worldMap/WorldMap.tsx @@ -0,0 +1,111 @@ +import { IData } from "./IData"; +import { IWorldMapProps } from "./IWorldMapProps"; +import MaplibreWorldMap from "./WorldMapControl"; +import React from "react"; + +const WorldMap: React.FC = (props) => { + + const countries: IData[] = React.useMemo( + () => [ + { id:"us", name:"Microsoft – USA", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-us", coordinates:[-95.7129,37.0902] }, + { id:"canada", name:"Microsoft – Canada", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ca", coordinates:[-79.3832,43.6532] }, + { id:"mexico", name:"Microsoft – Mexico", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/es-mx", coordinates:[-99.1332,19.4326] }, + { id:"brazil", name:"Microsoft – Brazil", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/pt-br", coordinates:[-46.6333,-23.5505] }, + // Europe + { id:"portugal", name:"Microsoft – Portugal", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/pt-pt", coordinates:[-9.1393,38.7223] }, + { id:"uk", name:"Microsoft – United Kingdom", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-gb", coordinates:[-0.1276,51.5074] }, + { id:"ireland", name:"Microsoft – Ireland", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ie", coordinates:[-6.2603,53.3498] }, + { id:"france", name:"Microsoft – France", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-fr", coordinates:[2.3522,48.8566] }, + { id:"germany", name:"Microsoft – Germany", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/de-de", coordinates:[11.5820,48.1351] }, + { id:"italy", name:"Microsoft – Italy", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/it-it", coordinates:[12.4964,41.9028] }, + { id:"spain", name:"Microsoft – Spain", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/es-es", coordinates:[-3.7038,40.4168] }, + { id:"sweden", name:"Microsoft – Sweden", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sv-se", coordinates:[18.0686,59.3293] }, + { id:"switzerland", name:"Microsoft – Switzerland", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/de-ch", coordinates:[7.4474,46.9480] }, + { id:"norway", name:"Microsoft – Norway", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/nb-no", coordinates:[10.7522,59.9139] }, + { id:"finland", name:"Microsoft – Finland", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fi-fi", coordinates:[24.9384,60.1699] }, + { id:"denmark", name:"Microsoft – Denmark", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/da-dk", coordinates:[12.5683,55.6761] }, + { id:"netherlands", name:"Microsoft – Netherlands", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/nl-nl", coordinates:[4.8952,52.3702] }, + { id:"belgium", name:"Microsoft – Belgium", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/nl-be", coordinates:[4.3517,50.8503] }, + { id:"luxembourg", name:"Microsoft – Luxembourg", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-lu", coordinates:[6.1319,49.6116] }, + { id:"austria", name:"Microsoft – Austria", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/de-at", coordinates:[16.3738,48.2082] }, + { id:"poland", name:"Microsoft – Poland", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/pl-pl", coordinates:[21.0122,52.2297] }, + { id:"czech_republic", name:"Microsoft – Czech Republic", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/cs-cz", coordinates:[14.4208,50.0880] }, + { id:"slovakia", name:"Microsoft – Slovakia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sk-sk", coordinates:[17.1077,48.1486] }, + { id:"hungary", name:"Microsoft – Hungary", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/hu-hu", coordinates:[19.0402,47.4979] }, + { id:"slovenia", name:"Microsoft – Slovenia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sl-si", coordinates:[14.5058,46.0569] }, + { id:"croatia", name:"Microsoft – Croatia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/hr-hr", coordinates:[15.9819,45.8150] }, + { id:"serbia", name:"Microsoft – Serbia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sr-latn-rs", coordinates:[20.4573,44.7872] }, + { id:"bosnia", name:"Microsoft – Bosnia and Herzegovina", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/bs-ba", coordinates:[18.4131,43.8563] }, + { id:"montenegro", name:"Microsoft – Montenegro", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sr-latn-me", coordinates:[19.2629,42.4304] }, + { id:"albania", name:"Microsoft – Albania", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/sq-al", coordinates:[19.8187,41.3275] }, + { id:"macedonia", name:"Microsoft – North Macedonia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/mk-mk", coordinates:[21.4316,41.9981] }, + { id:"greece", name:"Microsoft – Greece", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/el-gr", coordinates:[23.7275,37.9838] }, + { id:"bulgaria", name:"Microsoft – Bulgaria", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/bg-bg", coordinates:[23.3219,42.6977] }, + { id:"romania", name:"Microsoft – Romania", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ro-ro", coordinates:[26.1025,44.4268] }, + { id:"moldova", name:"Microsoft – Moldova", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ro-md", coordinates:[28.8638,47.0105] }, + { id:"ukraine", name:"Microsoft – Ukraine", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/uk-ua", coordinates:[30.5234,50.4501] }, + { id:"belarus", name:"Microsoft – Belarus", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/be-by", coordinates:[27.5615,53.9045] }, + { id:"estonia", name:"Microsoft – Estonia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/et-ee", coordinates:[24.7536,59.4370] }, + { id:"latvia", name:"Microsoft – Latvia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/lv-lv", coordinates:[24.1052,56.9496] }, + { id:"lithuania", name:"Microsoft – Lithuania", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/lt-lt", coordinates:[25.2797,54.6872] }, + // Rest of the world (original list) + { id:"russia", name:"Microsoft – Russia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ru-ru", coordinates:[37.6173,55.7558] }, + { id:"south_africa", name:"Microsoft – South Africa", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-za", coordinates:[28.0473,-26.2041] }, + { id:"uae", name:"Microsoft – UAE", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ae", coordinates:[55.2708,25.2048] }, + { id:"india", name:"Microsoft – India", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-in", coordinates:[78.9629,20.5937] }, + { id:"china", name:"Microsoft – China", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/zh-cn", coordinates:[116.383,39.917] }, + { id:"hong_kong", name:"Microsoft – Hong Kong", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/zh-hk", coordinates:[114.1095,22.3964] }, + { id:"japan", name:"Microsoft – Japan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ja-jp", coordinates:[139.6917,35.6895] }, + { id:"south_korea", name:"Microsoft – South Korea", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ko-kr", coordinates:[126.9780,37.5665] }, + { id:"australia", name:"Microsoft – Australia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-au", coordinates:[151.2093,-33.8688] }, + { id:"new_zealand", name:"Microsoft – New Zealand", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-nz", coordinates:[174.88597,-40.90056] }, + { id:"singapore", name:"Microsoft – Singapore", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-sg", coordinates:[103.8198,1.3521] }, + { id:"malaysia", name:"Microsoft – Malaysia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-my", coordinates:[101.6869,3.1390] }, + { id:"philippines", name:"Microsoft – Philippines", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ph", coordinates:[121.7740,12.8797] }, + { id:"thailand", name:"Microsoft – Thailand", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/th-th", coordinates:[100.5018,13.7563] }, + { id:"indonesia", name:"Microsoft – Indonesia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/id-id", coordinates:[106.8456,-6.2088] }, + { id:"vietnam", name:"Microsoft – Vietnam", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/vi-vn", coordinates:[105.8442,21.0278] }, + { id:"bangladesh", name:"Microsoft – Bangladesh", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/bn-bd", coordinates:[90.4125,23.8103] }, + { id:"pakistan", name:"Microsoft – Pakistan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ur-pk", coordinates:[67.0011,24.8607] }, + { id:"egypt", name:"Microsoft – Egypt", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-eg", coordinates:[31.2357,30.0444] }, + { id:"morocco", name:"Microsoft – Morocco", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-ma", coordinates:[-7.5898,33.5731] }, + { id:"kenya", name:"Microsoft – Kenya", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ke", coordinates:[36.8219,-1.2921] }, + { id:"ghana", name:"Microsoft – Ghana", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-gh", coordinates:[-0.1869,5.6037] }, + { id:"ivory_coast", name:"Microsoft – Côte d'Ivoire", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-ci", coordinates:[-4.0083,5.3453] }, + { id:"algeria", name:"Microsoft – Algeria", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-dz", coordinates:[3.0588,36.7538] }, + { id:"nigeria", name:"Microsoft – Nigeria", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-ng", coordinates:[3.3792,6.5244] }, + { id:"tunisia", name:"Microsoft – Tunisia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/fr-tn", coordinates:[10.1815,36.8065] }, + { id:"saudi_arabia", name:"Microsoft – Saudi Arabia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-sa", coordinates:[46.6753,24.7136] }, + { id:"qatar", name:"Microsoft – Qatar", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-qa", coordinates:[51.5310,25.2854] }, + { id:"kuwait", name:"Microsoft – Kuwait", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-kw", coordinates:[47.9783,29.3759] }, + { id:"oman", name:"Microsoft – Oman", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-om", coordinates:[58.3829,23.5880] }, + { id:"bahrain", name:"Microsoft – Bahrain", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-bh", coordinates:[50.5832,26.0667] }, + { id:"lebanon", name:"Microsoft – Lebanon", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-lb", coordinates:[35.5018,33.8938] }, + { id:"jordan", name:"Microsoft – Jordan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ar-jo", coordinates:[35.9106,31.9632] }, + { id:"israel", name:"Microsoft – Israel", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/he-il", coordinates:[34.7818,32.0853] }, + { id:"kazakhstan", name:"Microsoft – Kazakhstan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/kk-kz", coordinates:[76.8860,43.2389] }, + { id:"uzbekistan", name:"Microsoft – Uzbekistan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/uz-uz", coordinates:[69.2401,41.2995] }, + { id:"azerbaijan", name:"Microsoft – Azerbaijan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/az-latn-az", coordinates:[49.8671,40.4093] }, + { id:"georgia", name:"Microsoft – Georgia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ka-ge", coordinates:[44.7930,41.7151] }, + { id:"armenia", name:"Microsoft – Armenia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/hy-am", coordinates:[44.5090,40.1792] }, + { id:"turkmenistan", name:"Microsoft – Turkmenistan", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/tm-tm", coordinates:[58.3833,37.9500] }, + { id:"mongolia", name:"Microsoft – Mongolia", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/mn-mn", coordinates:[106.9155,47.8864] }, + { id:"sri_lanka", name:"Microsoft – Sri Lanka", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/en-lk", coordinates:[79.8612,6.9271] }, + { id:"nepal", name:"Microsoft – Nepal", imageUrl:"https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg", link:"https://www.microsoft.com/ne-np", coordinates:[85.3240,27.7172] } + ], + [] + ); + + return ( +
+ console.log('Clicked', c)} + title="Microsoft Locations Worldwide" + /> +
+ ); +}; + +export default WorldMap; diff --git a/src/controls/worldMap/WorldMapControl.tsx b/src/controls/worldMap/WorldMapControl.tsx new file mode 100644 index 000000000..6771b207c --- /dev/null +++ b/src/controls/worldMap/WorldMapControl.tsx @@ -0,0 +1,293 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import 'maplibre-gl/dist/maplibre-gl.css'; + +import Map, { MapRef, StyleSpecification } from 'react-map-gl/maplibre'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { SearchBox, Subtitle1, Text } from '@fluentui/react-components'; + +import { IData } from './IData'; +import { IMaplibreWorldMapProps } from './IMaplibreWorldMapProps'; +import MapNavigation from './MapNavigation'; +import Marker from './Marker'; +import { css } from '@emotion/css'; +import strings from 'ControlStrings'; +import { useCleanMapStyle } from './useCleanMapStyle'; + +const MULTI_STYLE_URLS = { + satellite: (key: string) => + `https://api.maptiler.com/maps/satellite/style.json?key=${key}`, + streets: (key: string) => + `https://api.maptiler.com/maps/streets/style.json?key=${key}`, + topo: (key: string) => + `https://api.maptiler.com/maps/topo-v2/style.json?key=${key}`, + demo: `https://demotiles.maplibre.org/style.json`, // Free demo style (no key required) +}; + +const useStyles = () => ({ + container: css({ + padding: '20px', + }), + mapContainer: css({ + position: 'relative', + marginTop: '20px', + }), + searchOverlay: css({ + position: 'absolute', + zIndex: 1000, + maxWidth: '300px', + padding: '8px', + }), + searchResults: css({ + marginTop: '4px', + fontSize: '12px', + color: '#666', + }), +}); + +/** + * Main Maplibre world map component. + * expects each data to already include a `coordinates: [lon, lat]` tuple. + */ +export const MaplibreWorldMap: React.FC = ({ + data, + onClick, + mapStyleUrl, + mapKey, + style, + fitPadding = 20, + theme, + marker, + title, + search, +}) => { + const mapRef = useRef(null); + const styles = useStyles(); + + // Determine the final map style URL based on provided props + const finalMapStyleUrl = useMemo(() => { + // If user provides both mapKey and mapStyleUrl, use the mapStyleUrl as-is (assuming it already includes the key) + if (mapKey && mapStyleUrl) { + // Check if the URL already contains a key parameter + if (mapStyleUrl.includes('?key=') || mapStyleUrl.includes('&key=')) { + return mapStyleUrl; + } else { + // Add the key to the URL + const separator = mapStyleUrl.includes('?') ? '&' : '?'; + return `${mapStyleUrl}${separator}key=${mapKey}`; + } + } + + // If only mapKey is provided, use the default streets style with the user's key + if (mapKey && !mapStyleUrl) { + return MULTI_STYLE_URLS.streets(mapKey); + } + + // If only mapStyleUrl is provided, use it as-is + if (!mapKey && mapStyleUrl) { + return mapStyleUrl; + } + + // If neither is provided, use the demo style (no key required) + return MULTI_STYLE_URLS.demo; + }, [mapKey, mapStyleUrl]); + + const cleanStyle = useCleanMapStyle(finalMapStyleUrl); + + const [searchTerm, setSearchTerm] = useState(''); + const [filteredData, setFilteredData] = useState(data); + const [initialViewState] = useState({ longitude: 0, latitude: 20, zoom: 1 }); + + // Search configuration with defaults + const searchConfig = useMemo( + () => ({ + enabled: search?.enabled ?? true, + placeholder: search?.placeholder ?? strings.worldMapSearchLocations, + searchField: search?.searchField ?? strings.worldMapSearchField, + zoomLevel: search?.zoomLevel ?? 8, + position: { + top: '10px', + left: '10px', + ...search?.position, + }, + ...search, + }), + [search] + ); + + // Reset to initial view when search is cleared + const resetToInitialView = useCallback(() => { + if (mapRef.current) { + if (data.length > 0) { + // Fit to all data + const lons = data.map((c) => c.coordinates[0]); + const lats = data.map((c) => c.coordinates[1]); + mapRef.current.getMap().fitBounds( + [ + [Math.min(...lons), Math.min(...lats)], + [Math.max(...lons), Math.max(...lats)], + ], + { padding: fitPadding, duration: 1000 } + ); + } else { + // Reset to initial view state + mapRef.current.getMap().flyTo({ + center: [initialViewState.longitude, initialViewState.latitude], + zoom: initialViewState.zoom, + duration: 1000, + }); + } + } + }, [data, fitPadding, initialViewState]); + + // Filter data based on search term + const handleSearch = useCallback( + (term: string) => { + setSearchTerm(term); + + if (!term.trim()) { + setFilteredData(data); + search?.onSearchChange?.(term, data); + resetToInitialView(); + return; + } + + const filtered = data.filter((item) => { + const searchValue = + typeof searchConfig.searchField === 'function' + ? searchConfig.searchField(item) + : (item[searchConfig.searchField as keyof IData] as string); + + return searchValue?.toLowerCase().includes(term.toLowerCase()); + }); + + setFilteredData(filtered); + search?.onSearchChange?.(term, filtered); + + // Auto-zoom to first result + if (filtered.length > 0 && mapRef.current) { + const firstResult = filtered[0]; + mapRef.current.getMap().flyTo({ + center: firstResult.coordinates, + zoom: searchConfig.zoomLevel, + duration: 1000, + }); + } + }, + [data, search, searchConfig, resetToInitialView] + ); + + // Handle search clear + const handleSearchClear = useCallback(() => { + setSearchTerm(''); + setFilteredData(data); + search?.onSearchChange?.('', data); + resetToInitialView(); + }, [data, search, resetToInitialView]); + + // Update filtered data when data prop changes + useEffect(() => { + if (!searchTerm.trim()) { + setFilteredData(data); + } else { + handleSearch(searchTerm); + } + }, [data, searchTerm, handleSearch]); + + const defaultMapStyles: React.CSSProperties = useMemo( + () => ({ + width: '100%', + height: '600px', + fontFamily: theme?.fontFamilyBase, + paddingTop: '20px', + paddingBottom: '20px', + }), + [theme] + ); + + // Fit map to loaded markers + useEffect(() => { + if (!mapRef.current || filteredData.length === 0) return; + const lons = filteredData.map((c) => c.coordinates[0]); + const lats = filteredData.map((c) => c.coordinates[1]); + mapRef.current.getMap().fitBounds( + [ + [Math.min(...lons), Math.min(...lats)], + [Math.max(...lons), Math.max(...lats)], + ], + { padding: fitPadding } + ); + }, [filteredData, fitPadding]); + + if (!cleanStyle) { + return {strings.worldMapLoadintText}; + } + + return ( +
+ {title ?? strings.worldMapTitle} +
+ + + {filteredData.map((dataItem) => ( + onClick?.(dataItem)} + {...marker} + /> + ))} + + + {searchConfig.enabled && ( +
+ handleSearch(data?.value || '')} + dismiss={{ + onClick: handleSearchClear, + }} + size="medium" + style={{ width: '100%', minWidth: '250px' }} + /> + {searchTerm && ( +
+ {filteredData.length}{' '} + {filteredData.length === 1 + ? strings.worldMapLocationLabel + : strings.worldMapLocationPluralLabel}{' '} + {strings.worldMapFoundLabel} +
+ )} +
+ )} +
+
+ ); +}; + +export default MaplibreWorldMap; diff --git a/src/controls/worldMap/examples.tsx b/src/controls/worldMap/examples.tsx new file mode 100644 index 000000000..99d3f04c4 --- /dev/null +++ b/src/controls/worldMap/examples.tsx @@ -0,0 +1,217 @@ +// Example usage of WorldMapControl with search functionality + +import { IData } from './IData'; +import { MaplibreWorldMap } from './WorldMapControl'; +import React from 'react'; + +const ExampleWithSearch: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + }, + { + id: '2', + name: 'London', + imageUrl: 'https://example.com/london.jpg', + link: 'https://example.com/london', + coordinates: [-0.1276, 51.5074] + }, + { + id: '3', + name: 'Tokyo', + imageUrl: 'https://example.com/tokyo.jpg', + link: 'https://example.com/tokyo', + coordinates: [139.6917, 35.6895] + } + ]; + + const handleLocationClick = (location: IData): void => { + console.log('Location clicked:', location.name); + }; + + const handleSearchChange = (searchTerm: string, filteredData: IData[]): void => { + console.log('Search term:', searchTerm); + console.log('Filtered results:', filteredData); + if (!searchTerm) { + console.log('Search cleared - map reset to initial view'); + } + }; + + return ( + item.name + position: { + top: '10px', + left: '10px' + } + }} + style={{ + width: '100%', + height: '500px' + }} + /> + ); +}; + +// Example with custom positioning (top-right) +const ExampleWithTopRightSearch: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + } + ]; + + return ( + + ); +}; + +// Example with custom MapTiler API key +const ExampleWithCustomMapKey: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + } + ]; + + return ( + + ); +}; + +// Example with custom MapTiler API key and specific style +const ExampleWithCustomKeyAndStyle: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + } + ]; + + return ( + + ); +}; + +// Example using demo map (no key required) +const ExampleWithDemoMap: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + } + ]; + + return ( + + ); +}; + +// Example with search disabled +const ExampleWithoutSearch: React.FC = () => { + const mapData: IData[] = [ + // ... your data + ]; + + return ( + + ); +}; + +// Example with custom search field +const ExampleWithCustomSearch: React.FC = () => { + const mapData: IData[] = [ + { + id: '1', + name: 'New York', + imageUrl: 'https://example.com/ny.jpg', + link: 'https://example.com/ny', + coordinates: [-74.006, 40.7128] + } + ]; + + return ( + `${item.name} ${item.link}`, + }} + /> + ); +}; + +export { + ExampleWithSearch, + ExampleWithCustomSearch, + ExampleWithoutSearch, + ExampleWithTopRightSearch, + ExampleWithCustomMapKey, + ExampleWithCustomKeyAndStyle, + ExampleWithDemoMap +}; diff --git a/src/controls/worldMap/index.ts b/src/controls/worldMap/index.ts new file mode 100644 index 000000000..ac71f825f --- /dev/null +++ b/src/controls/worldMap/index.ts @@ -0,0 +1,9 @@ +export * from './IData'; +export * from './IWorldMapProps'; +export * from './IMaplibreWorldMapProps'; +export * from './WorldMap'; +export * from './WorldMapControl'; + + + + diff --git a/src/controls/worldMap/useCleanMapStyle.tsx b/src/controls/worldMap/useCleanMapStyle.tsx new file mode 100644 index 000000000..2137827ec --- /dev/null +++ b/src/controls/worldMap/useCleanMapStyle.tsx @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { useEffect, useState } from "react"; + +import type { Style } from "maplibre-gl"; + +/** + * Fetches and cleans a MapLibre style JSON, removing any graticule layer. + */ +export const useCleanMapStyle = (url: string): Style | undefined => { + const [styleJson, setStyleJson] = useState