diff --git a/src/actions/index.js b/src/actions/index.js index ae3939d..7378921 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -421,3 +421,13 @@ export function rehydrateState() { 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, + }; +} diff --git a/src/components/space/carto/Map.jsx b/src/components/space/carto/Map.jsx index 0b32999..bf6858f 100644 --- a/src/components/space/carto/Map.jsx +++ b/src/components/space/carto/Map.jsx @@ -54,6 +54,7 @@ class Map extends Component { clusters: [], }; this.styleLocation = this.styleLocation.bind(this); + this.syncMapViewToUrl = this.syncMapViewToUrl.bind(this); } componentDidMount() { @@ -75,6 +76,25 @@ class Map extends Component { 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 const { bounds } = nextProps.app.map; if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) { @@ -167,6 +187,7 @@ class Map extends Component { map.on("moveend", () => { this.alignLayers(); this.updateClusters(); + this.syncMapViewToUrl(); }); map.on("zoomend viewreset", () => { @@ -205,6 +226,16 @@ class Map extends Component { 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() { const [bbox, zoom] = this.getMapDetails(); if (this.superclusterIndex && this.state.indexLoaded) { diff --git a/src/reducers/app.js b/src/reducers/app.js index 16e6c0c..37036ec 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -31,6 +31,7 @@ import { SET_INITIAL_CATEGORIES, SET_INITIAL_SHAPES, UPDATE_SEARCH_QUERY, + UPDATE_MAP_VIEW, } from "../actions"; 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) { switch (action.type) { case UPDATE_HIGHLIGHTED: @@ -368,6 +380,8 @@ function app(appState = initial.app, action) { return setInitialShapes(appState, action); case UPDATE_SEARCH_QUERY: return updateSearchQuery(appState, action); + case UPDATE_MAP_VIEW: + return updateMapView(appState, action); default: return appState; } diff --git a/src/selectors/index.js b/src/selectors/index.js index 435480f..7b15802 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -43,6 +43,9 @@ export const getEventRadius = (state) => state.ui.eventRadius; export const getTile = (state) => state.ui.tiles.current; export const isUsingSatellite = (state) => 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( [getSites, getFeatures], diff --git a/src/store/plugins/urlState/schema.js b/src/store/plugins/urlState/schema.js index 8acc2bd..b4ac325 100644 --- a/src/store/plugins/urlState/schema.js +++ b/src/store/plugins/urlState/schema.js @@ -3,6 +3,7 @@ import { UPDATE_COLORING_SET, UPDATE_SELECTED, UPDATE_TIMERANGE, + UPDATE_MAP_VIEW, } from "../../../actions"; import { ASSOCIATION_MODES } from "../../../common/constants"; import { createFilterPathString } from "../../../common/utilities"; @@ -11,6 +12,9 @@ import { getTimeRange, selectActiveColorSets, selectActiveFilterIds, + getMapLat, + getMapLng, + getMapZoom, } from "../../../selectors"; 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) {