mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-07 19:08:37 +03:00
Synchronize map state to URL (#84)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user