Skip to content

WorldMap Control #2023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/documentation/docs/assets/WorldMap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
423 changes: 423 additions & 0 deletions docs/documentation/docs/controls/WorldMap.md

Large diffs are not rendered by default.

546 changes: 532 additions & 14 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/WorldMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controls/worldMap/index';
40 changes: 40 additions & 0 deletions src/controls/worldMap/IData.ts
Original file line number Diff line number Diff line change
@@ -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];
}
241 changes: 241 additions & 0 deletions src/controls/worldMap/IMaplibreWorldMapProps.tsx
Original file line number Diff line number Diff line change
@@ -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={<h2>Interactive Location Map</h2>}
* ```
*/
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) => <div><strong>{item.name}</strong><br/>{item.description}</div>}
* ```
*/
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();
* <MaplibreWorldMap theme={theme} />
* ```
*/
theme?: Theme
}
10 changes: 10 additions & 0 deletions src/controls/worldMap/IMarker.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/controls/worldMap/IMarkerProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IData } from './IData';
import { IMarker } from './IMarker';

export interface IMarkerProps extends IMarker {
data: IData;
onClick?: (data: IData) => void;
}
13 changes: 13 additions & 0 deletions src/controls/worldMap/IWorldMapProps.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading