Skip to content

Add support for getting map state#3

Merged
giswqs merged 1 commit intomainfrom
state
Jan 18, 2026
Merged

Add support for getting map state#3
giswqs merged 1 commit intomainfrom
state

Conversation

@giswqs
Copy link
Copy Markdown
Member

@giswqs giswqs commented Jan 18, 2026

No description provided.

Copilot AI review requested due to automatic review settings January 18, 2026 05:44
@giswqs giswqs merged commit 7323c9d into main Jan 18, 2026
6 checks passed
@giswqs giswqs deleted the state branch January 18, 2026 05:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive map state management functionality to maplibre-gl-extend, enabling capture and restoration of complete map state including camera position, style, terrain, sky, projection, layers, and controls. This allows for use cases like state persistence, undo/redo functionality, and sharing map configurations.

Changes:

  • Added state management API with getMapState() and setMapState() methods for capturing and restoring map state
  • Implemented control tracking system with addTrackedControl(), removeTrackedControl(), and getTrackedControls() methods
  • Extended ID generation utilities to support control ID generation

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/types.d.ts Added type definitions for state management methods and control tracking to the Map interface
src/lib/utils/index.ts Exported new generateControlId utility function
src/lib/utils/id-generator.ts Added control ID generation with counter tracking
src/lib/state/types.ts Defined comprehensive types for map state, camera state, control info, and serialization options
src/lib/state/capture.ts Implemented getMapState() to capture complete map state with configurable options
src/lib/state/restore.ts Implemented setMapState() to restore map state asynchronously with style loading and animation support
src/lib/state/controls.ts Implemented control registry system for tracking, serializing, and restoring map controls
src/lib/state/index.ts Exported all state management types and functions
src/index.ts Integrated state management methods into Map prototype and exported new types
README.md Added comprehensive documentation for state management features with examples
.vite/deps/package.json Added Vite dependency cache metadata (should not be in version control)
.vite/deps/_metadata.json Added Vite dependency cache metadata (should not be in version control)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .vite/deps/_metadata.json
Comment on lines +2 to +5
"hash": "2ab3563f",
"configHash": "eb5124d0",
"lockfileHash": "5cab8a34",
"browserHash": "232aee30",
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .vite/deps directory contains Vite-generated build artifacts that should not be committed to version control. These files are automatically generated during development and will differ between environments. Add .vite/ to .gitignore to prevent these files from being tracked.

Suggested change
"hash": "2ab3563f",
"configHash": "eb5124d0",
"lockfileHash": "5cab8a34",
"browserHash": "232aee30",
"hash": "",
"configHash": "",
"lockfileHash": "",
"browserHash": "",

Copilot uses AI. Check for mistakes.
Comment thread src/lib/state/restore.ts
Comment on lines +52 to +55
// 2. Restore basemap (if using our basemap system)
if (state.basemap) {
setBasemap(map, state.basemap);
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a potential conflict when restoring state with a basemap. If restoreStyle is true and the captured style already includes the basemap layer (from the __maplibre-extend-basemap source/layer), calling setBasemap here will attempt to add the basemap again, potentially causing duplication or conflicts. Consider checking if the style restoration should be skipped when a basemap is present in the state, or handle the basemap restoration more carefully to avoid conflicts with the style that was just restored.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/state/capture.ts
Comment on lines +41 to +108
export function getMapState(
map: Map,
options: GetMapStateOptions = {}
): MapState {
const {
includeCamera = true,
includeStyle = true,
includeTerrain = true,
includeSky = true,
includeProjection = true,
includeControls = true,
includeCustomLayers = true,
metadata,
} = options;

// Get style - this includes sources and layers
const style = includeStyle
? (map.getStyle() as StyleSpecification)
: ({
version: 8,
sources: {},
layers: [],
} as StyleSpecification);

// Get camera state
const camera: CameraState = includeCamera
? captureCamera(map)
: {
center: [0, 0],
zoom: 0,
bearing: 0,
pitch: 0,
};

// Get terrain
const terrain = includeTerrain ? map.getTerrain() : null;

// Get sky
const sky = includeSky ? map.getSky() : null;

// Get projection
const projection: ProjectionSpecification = includeProjection
? map.getProjection()
: { type: 'mercator' };

// Get basemap from our registry
const basemap = getBasemap(map);

// Get custom layers from our registry
const customLayers = includeCustomLayers ? getAllCustomLayers(map) : [];

// Get tracked controls
const controls = includeControls ? getSerializableControls(map) : [];

return {
version: STATE_VERSION,
timestamp: Date.now(),
camera,
style,
terrain,
sky,
projection,
basemap,
customLayers,
controls,
metadata,
};
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code lacks test coverage. The state management functionality (getMapState, setMapState, control tracking) is a significant new feature that should have comprehensive test coverage to ensure it works correctly across different scenarios (partial restore, control restoration, error handling, etc.).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/state/restore.ts
Comment on lines +15 to +130
export async function setMapState(
map: Map,
state: MapState,
options: SetMapStateOptions = {}
): Promise<void> {
const {
restoreCamera = true,
restoreStyle = true,
restoreTerrain = true,
restoreSky = true,
restoreProjection = true,
restoreControls: shouldRestoreControls = false,
controlFactory,
cameraAnimation = { animate: false, duration: 1000 },
onComplete,
onError,
} = options;

try {
// 1. Restore style (this must be first and we wait for it to load)
if (restoreStyle && state.style) {
await new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Style load timeout'));
}, 30000); // 30 second timeout

const onStyleLoad = () => {
clearTimeout(timeoutId);
map.off('style.load', onStyleLoad);
resolve();
};

map.on('style.load', onStyleLoad);
map.setStyle(state.style);
});
}

// 2. Restore basemap (if using our basemap system)
if (state.basemap) {
setBasemap(map, state.basemap);
}

// 3. Restore projection
if (restoreProjection && state.projection) {
map.setProjection(state.projection);
}

// 4. Restore terrain
if (restoreTerrain) {
if (state.terrain) {
map.setTerrain(state.terrain);
} else {
map.setTerrain(null);
}
}

// 5. Restore sky
if (restoreSky) {
if (state.sky) {
map.setSky(state.sky);
} else {
// Clear sky by setting empty object
map.setSky({} as Parameters<typeof map.setSky>[0]);
}
}

// 6. Restore camera
if (restoreCamera && state.camera) {
const cameraOptions = {
center: state.camera.center as [number, number],
zoom: state.camera.zoom,
bearing: state.camera.bearing,
pitch: state.camera.pitch,
padding: state.camera.padding,
};

if (cameraAnimation.animate) {
await new Promise<void>((resolve) => {
map.once('moveend', () => resolve());
map.flyTo({
...cameraOptions,
duration: cameraAnimation.duration ?? 1000,
});
});
} else {
map.jumpTo(cameraOptions);
}
}

// 7. Restore layer registry metadata
if (state.customLayers && state.customLayers.length > 0) {
restoreLayerRegistry(map, state);
}

// 8. Restore controls
if (shouldRestoreControls && state.controls && state.controls.length > 0) {
if (!controlFactory) {
console.warn(
'restoreControls is true but no controlFactory provided. Controls will not be restored.'
);
} else {
restoreControls(map, state.controls, controlFactory);
}
} else if (!shouldRestoreControls) {
// Clear tracked controls if not restoring them
clearAllTrackedControls(map);
}

onComplete?.();
} catch (error) {
const err =
error instanceof Error ? error : new Error(String(error));
onError?.(err);
throw err;
}
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code lacks test coverage. The state restoration functionality is complex and asynchronous with many edge cases (timeout handling, style loading, camera animation, control restoration failures) that should be tested to ensure reliability.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/state/controls.ts
Comment on lines +41 to +180
export function storeControlInfo(
map: Map,
control: IControl,
position: ControlPosition = 'top-right',
type: string = 'unknown',
options?: Record<string, unknown>
): string {
const id = generateControlId(type.toLowerCase());
const registry = getControlRegistry(map);

registry[id] = {
id,
type,
position,
options,
instance: control,
};

(map as unknown as Record<string, ControlRegistry>)[CONTROL_REGISTRY_KEY] =
registry;

// Add the control to the map
map.addControl(control, position);

return id;
}

/**
* Remove control info from the registry and remove the control from the map.
*
* @param map - MapLibre map instance
* @param controlId - Control ID to remove
* @returns True if control was found and removed
*/
export function removeControlInfo(map: Map, controlId: string): boolean {
const registry = getControlRegistry(map);
const controlInfo = registry[controlId];

if (!controlInfo) {
return false;
}

// Remove the control from the map
map.removeControl(controlInfo.instance);

// Remove from registry
delete registry[controlId];
(map as unknown as Record<string, ControlRegistry>)[CONTROL_REGISTRY_KEY] =
registry;

return true;
}

/**
* Get all tracked controls (with instance references).
*
* @param map - MapLibre map instance
* @returns Array of control info
*/
export function getAllTrackedControls(map: Map): ControlInfo[] {
const registry = getControlRegistry(map);
return Object.values(registry);
}

/**
* Get serializable control info (without instance references) for state storage.
*
* @param map - MapLibre map instance
* @returns Array of serializable control info
*/
export function getSerializableControls(map: Map): SerializableControlInfo[] {
const registry = getControlRegistry(map);
return Object.values(registry).map(({ id, type, position, options }) => ({
id,
type,
position,
options,
}));
}

/**
* Clear all tracked controls from the map.
*
* @param map - MapLibre map instance
*/
export function clearAllTrackedControls(map: Map): void {
const registry = getControlRegistry(map);

for (const controlInfo of Object.values(registry)) {
try {
map.removeControl(controlInfo.instance);
} catch {
// Control may already be removed
}
}

(map as unknown as Record<string, ControlRegistry>)[CONTROL_REGISTRY_KEY] =
{};
}

/**
* Restore control registry from serialized state.
* Note: This only restores the registry metadata, not the actual controls.
* Use a controlFactory with setMapState to recreate controls.
*
* @param map - MapLibre map instance
* @param controls - Serializable control info array
* @param controlFactory - Factory function to recreate controls
*/
export function restoreControls(
map: Map,
controls: SerializableControlInfo[],
controlFactory: (info: SerializableControlInfo) => IControl | null
): void {
// Clear existing tracked controls
clearAllTrackedControls(map);

const registry: ControlRegistry = {};

for (const serializedInfo of controls) {
const control = controlFactory(serializedInfo);

if (control) {
// Add control to map
map.addControl(control, serializedInfo.position);

// Store in registry with the original ID
registry[serializedInfo.id] = {
id: serializedInfo.id,
type: serializedInfo.type,
position: serializedInfo.position,
options: serializedInfo.options,
instance: control,
};
}
}

(map as unknown as Record<string, ControlRegistry>)[CONTROL_REGISTRY_KEY] =
registry;
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code lacks test coverage. The control tracking system (adding, removing, restoring controls) is a new feature that should have tests to verify the registry operations work correctly and controls are properly managed.

Copilot uses AI. Check for mistakes.
Comment thread .vite/deps/package.json
Comment on lines +1 to +3
{
"type": "module"
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .vite/deps directory contains Vite-generated build artifacts that should not be committed to version control. These files are automatically generated during development and will differ between environments. Add .vite/ to .gitignore to prevent these files from being tracked.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants