Synchronize map state to URL (#84)

This commit is contained in:
Logan Williams
2025-12-11 09:09:11 -05:00
committed by GitHub
parent 7f5b0d9c19
commit 6fab980c46
5 changed files with 110 additions and 0 deletions

View File

@@ -421,3 +421,13 @@ export function rehydrateState() {
type: REHYDRATE_STATE, type: REHYDRATE_STATE,
}; };
} }
export const UPDATE_MAP_VIEW = "UPDATE_MAP_VIEW";
export function updateMapView(lat, lng, zoom) {
return {
type: UPDATE_MAP_VIEW,
lat,
lng,
zoom,
};
}

View File

@@ -54,6 +54,7 @@ class Map extends Component {
clusters: [], clusters: [],
}; };
this.styleLocation = this.styleLocation.bind(this); this.styleLocation = this.styleLocation.bind(this);
this.syncMapViewToUrl = this.syncMapViewToUrl.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -75,6 +76,25 @@ class Map extends Component {
this.loadClusterData(nextProps.domain.locations); this.loadClusterData(nextProps.domain.locations);
} }
// Update map view if anchor or zoom changed (e.g., from URL state rehydration)
const { anchor: nextAnchor, startZoom: nextZoom } = nextProps.app.map;
const { anchor: currAnchor, startZoom: currZoom } = this.props.app.map;
if (
this.map &&
(!isIdentical(nextAnchor, currAnchor) || nextZoom !== currZoom)
) {
const currentCenter = this.map.getCenter();
const currentZoom = this.map.getZoom();
// Only update if the values actually differ from the current map state
if (
Math.abs(currentCenter.lat - nextAnchor[0]) > 0.00001 ||
Math.abs(currentCenter.lng - nextAnchor[1]) > 0.00001 ||
currentZoom !== nextZoom
) {
this.map.setView(nextAnchor, nextZoom, { animate: false });
}
}
// Set appropriate zoom for narrative // Set appropriate zoom for narrative
const { bounds } = nextProps.app.map; const { bounds } = nextProps.app.map;
if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) { if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) {
@@ -167,6 +187,7 @@ class Map extends Component {
map.on("moveend", () => { map.on("moveend", () => {
this.alignLayers(); this.alignLayers();
this.updateClusters(); this.updateClusters();
this.syncMapViewToUrl();
}); });
map.on("zoomend viewreset", () => { map.on("zoomend viewreset", () => {
@@ -205,6 +226,16 @@ class Map extends Component {
return [bbox, zoom]; return [bbox, zoom];
} }
syncMapViewToUrl() {
if (!this.map) return;
const center = this.map.getCenter();
const zoom = this.map.getZoom();
// Round to 5 decimal places for cleaner URLs
const lat = Math.round(center.lat * 100000) / 100000;
const lng = Math.round(center.lng * 100000) / 100000;
this.props.actions.updateMapView(lat, lng, zoom);
}
updateClusters() { updateClusters() {
const [bbox, zoom] = this.getMapDetails(); const [bbox, zoom] = this.getMapDetails();
if (this.superclusterIndex && this.state.indexLoaded) { if (this.superclusterIndex && this.state.indexLoaded) {

View File

@@ -31,6 +31,7 @@ import {
SET_INITIAL_CATEGORIES, SET_INITIAL_CATEGORIES,
SET_INITIAL_SHAPES, SET_INITIAL_SHAPES,
UPDATE_SEARCH_QUERY, UPDATE_SEARCH_QUERY,
UPDATE_MAP_VIEW,
} from "../actions"; } from "../actions";
function updateHighlighted(appState, action) { function updateHighlighted(appState, action) {
@@ -310,6 +311,17 @@ function updateSearchQuery(appState, action) {
}; };
} }
function updateMapView(appState, action) {
return {
...appState,
map: {
...appState.map,
anchor: [action.lat, action.lng],
startZoom: action.zoom,
},
};
}
function app(appState = initial.app, action) { function app(appState = initial.app, action) {
switch (action.type) { switch (action.type) {
case UPDATE_HIGHLIGHTED: case UPDATE_HIGHLIGHTED:
@@ -368,6 +380,8 @@ function app(appState = initial.app, action) {
return setInitialShapes(appState, action); return setInitialShapes(appState, action);
case UPDATE_SEARCH_QUERY: case UPDATE_SEARCH_QUERY:
return updateSearchQuery(appState, action); return updateSearchQuery(appState, action);
case UPDATE_MAP_VIEW:
return updateMapView(appState, action);
default: default:
return appState; return appState;
} }

View File

@@ -43,6 +43,9 @@ export const getEventRadius = (state) => state.ui.eventRadius;
export const getTile = (state) => state.ui.tiles.current; export const getTile = (state) => state.ui.tiles.current;
export const isUsingSatellite = (state) => export const isUsingSatellite = (state) =>
state.ui.tiles.current === state.ui.tiles.satellite; state.ui.tiles.current === state.ui.tiles.satellite;
export const getMapLat = (state) => state.app.map.anchor[0];
export const getMapLng = (state) => state.app.map.anchor[1];
export const getMapZoom = (state) => state.app.map.startZoom;
export const selectSites = createSelector( export const selectSites = createSelector(
[getSites, getFeatures], [getSites, getFeatures],

View File

@@ -3,6 +3,7 @@ import {
UPDATE_COLORING_SET, UPDATE_COLORING_SET,
UPDATE_SELECTED, UPDATE_SELECTED,
UPDATE_TIMERANGE, UPDATE_TIMERANGE,
UPDATE_MAP_VIEW,
} from "../../../actions"; } from "../../../actions";
import { ASSOCIATION_MODES } from "../../../common/constants"; import { ASSOCIATION_MODES } from "../../../common/constants";
import { createFilterPathString } from "../../../common/utilities"; import { createFilterPathString } from "../../../common/utilities";
@@ -11,6 +12,9 @@ import {
getTimeRange, getTimeRange,
selectActiveColorSets, selectActiveColorSets,
selectActiveFilterIds, selectActiveFilterIds,
getMapLat,
getMapLng,
getMapZoom,
} from "../../../selectors"; } from "../../../selectors";
export const SCHEMA_TYPES = { export const SCHEMA_TYPES = {
@@ -128,6 +132,54 @@ export const SCHEMA = Object.freeze({
} }
}, },
}, },
lat: {
key: "lat",
trigger: UPDATE_MAP_VIEW,
type: SCHEMA_TYPES.NUMBER,
dehydrate(state) {
return getMapLat(state);
},
rehydrate(state, { lat }) {
if (lat != null && state.app.map) {
state.app.map = {
...state.app.map,
anchor: [lat, state.app.map.anchor[1]],
};
}
},
},
lng: {
key: "lng",
trigger: UPDATE_MAP_VIEW,
type: SCHEMA_TYPES.NUMBER,
dehydrate(state) {
return getMapLng(state);
},
rehydrate(state, { lng }) {
if (lng != null && state.app.map) {
state.app.map = {
...state.app.map,
anchor: [state.app.map.anchor[0], lng],
};
}
},
},
zoom: {
key: "zoom",
trigger: UPDATE_MAP_VIEW,
type: SCHEMA_TYPES.NUMBER,
dehydrate(state) {
return getMapZoom(state);
},
rehydrate(state, { zoom }) {
if (zoom != null && state.app.map) {
state.app.map = {
...state.app.map,
startZoom: zoom,
};
}
},
},
}); });
function mapFilterIdsToPaths(filters) { function mapFilterIdsToPaths(filters) {