Skip to content

Commit

Permalink
Watch hash for changes and update application state accordingly (#75)
Browse files Browse the repository at this point in the history
* Watch hash for changes and update app state if it differs

* Update CHANGELOG

* getHashString -> createHashString

* Avoid accessing element that doesn't exist yet

* Update mapsStore

* Move more hash logic into query

* Update dropdown when maps change

* Avoid touching current map settings

* Make config, settings, maps, mapState more clear
  • Loading branch information
ebrelsford authored Jun 6, 2022
1 parent 2fdf71e commit 2d7435a
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 82 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Unreleased

- Retain branch name when changing between branch patterns
- Retain branch name when changing between branch patterns (#72)
- Watch hash for changes, update app state on change (#75)

## 0.6.0

Expand Down
81 changes: 46 additions & 35 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,30 @@
import { loadPresetsFromUrl } from './presets-utils';
import Maps from './components/Maps.svelte';
import MapControls from './components/MapControls.svelte';
import { writeHash } from './query';
import { getInitialSettings } from './settings';
import { createHashString, writeHash } from './query';
import { getSettings } from './settings';
import { validateMapState } from './map-state-utils';
import throttle from 'lodash.throttle';
export let localConfig;
let mapState = {};
let maps = [];
// config is set only once on load and is assumed to be loaded from a module
const config = makeConfig(localConfig);
let settings = getInitialSettings(config);
let mapboxGlAccessToken;
let stylePresetUrls;
const { mapboxGlAccessToken, stylePresetUrls } = config;
configStore.set(config);
({ mapboxGlAccessToken, stylePresetUrls } = config);
mapsStore.subscribe(value => (maps = value));
// settings contains all of the current state of the app that we might want to
// persist in the URL
let settings = getSettings(config);
// Throttle writing to the hash since this can get invoked many times when
// moving the map around
const throttledWriteHash = throttle(() => {
writeHash({ ...settings, ...mapState });
}, 250);
// mapState is a convenience object, subset of settings
let mapState = {};
onMount(() => {
// Set maps and presets initially using settings
mapsStore.set(settings.maps.map((map, index) => ({ ...map, index })));
stylePresetsStore.set(settings.stylePresets);
// Set maps and presets initially using settings
mapsStore.set(settings.maps.map((map, index) => ({ ...map, index })));
stylePresetsStore.set(settings.stylePresets);
onMount(() => {
// If we have URLs for preset files, get them and update presets
if (stylePresetUrls.length > 0) {
stylePresetUrls.forEach(async url => {
Expand All @@ -48,18 +43,30 @@
}
});
$: if (settings && mapState) throttledWriteHash();
$: if (maps) {
// Remove the stylesheet for a more concise hash
const mapsHash = JSON.parse(JSON.stringify(maps)).map(m => {
delete m.style;
return m;
});
// Detect changes in hash and update settings appropriately
window.addEventListener('hashchange', () => {
if (location.hash.slice(1) !== createHashString(settings)) {
settings = getSettings(config);
writeHash({ ...settings, maps: mapsHash, ...mapState });
}
// Update mapsStore if necessary
if (settings.maps.length) {
const newMaps = settings.maps.map((map, index) => ({ ...map, index }));
mapsStore.set(newMaps);
}
}
});
mapsStore.subscribe(maps => {
settings = { ...settings, maps };
});
// Throttle writing to the hash since this can get invoked many times when
// moving the map around
const throttledWriteHash = throttle(() => writeHash(settings), 250);
$: if (settings) throttledWriteHash();
$: {
const createMapState = () => {
const {
bearing,
center,
Expand All @@ -70,7 +77,8 @@
height,
width,
} = settings;
mapState = {
return {
bearing,
center,
pitch,
Expand All @@ -80,32 +88,35 @@
...(height && { height }),
...(width && { width }),
};
}
};
$: if (settings || height || width) mapState = createMapState();
// Validate map state when maps change too
$: mapState = validateMapState(mapState, maps);
$: mapState = validateMapState(mapState, settings.maps);
const handleMapState = event => {
let newMapState = {
...mapState,
...event.detail.options,
};
mapState = validateMapState(newMapState, maps);
settings = {
...settings,
...validateMapState(newMapState, settings.maps),
};
};
const handleViewMode = event => {
settings = {
...settings,
...mapState,
viewMode: event.detail.mode,
};
};
const handleDimensions = event => {
settings = {
...settings,
...mapState,
// contains width and height
...event.detail,
};
Expand All @@ -122,7 +133,7 @@
</svelte:head>
<main>
<Maps
{maps}
maps={settings.maps}
{mapState}
viewMode={settings.viewMode}
on:mapState={handleMapState}
Expand Down
69 changes: 35 additions & 34 deletions src/components/MapStyleInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,6 @@
let pattern;
let allowPolling = true;
mapsStore.subscribe(maps => {
map = maps.find(m => m.index === index);
if (map) {
branch = map.branch;
pattern = map.pattern;
name = map.name;
url = map.url;
}
});
let stylePresets;
let branchPatterns;
Expand All @@ -52,30 +42,6 @@
return stylePresets && stylePresets.find(s => s.url === url);
};
const setSelected = () => {
const stylePresetOption = getStylePresetOption();
if (stylePresetOption) {
selected = { ...stylePresetOption, dropdownType: 'preset' };
textInput = '';
} else if (branch) {
selected = {
name,
dropdownType: 'branch',
url,
pattern,
};
textInput = branch;
} else {
selected = {
name,
dropdownType: 'custom',
url,
};
textInput = url;
}
dropdownOptions = getDropdownOptions();
};
$: if (stylePresets) {
setSelected();
}
Expand Down Expand Up @@ -268,6 +234,41 @@
return options;
};
const setSelected = () => {
const stylePresetOption = getStylePresetOption();
if (stylePresetOption) {
selected = { ...stylePresetOption, dropdownType: 'preset' };
textInput = '';
} else if (branch) {
selected = {
name,
dropdownType: 'branch',
url,
pattern,
};
textInput = branch;
} else {
selected = {
name,
dropdownType: 'custom',
url,
};
textInput = url;
}
dropdownOptions = getDropdownOptions();
};
mapsStore.subscribe(maps => {
map = maps.find(m => m.index === index);
if (map) {
branch = map.branch;
pattern = map.pattern;
name = map.name;
url = map.url;
}
setSelected();
});
</script>
<div class="map-style-input">
Expand Down
5 changes: 2 additions & 3 deletions src/components/MapboxGlMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,8 @@
.addEventListener('wheel', throttledWheelHandler, { passive: true });
const handleMove = ({ origin }) => {
const isFocused = document
.getElementById(id)
.contains(document.activeElement);
const isFocused =
document.getElementById(id)?.contains(document.activeElement) ?? false;
if (isFocused) {
dispatch('mapMove', { options: getCurrentMapView() });
}
Expand Down
31 changes: 23 additions & 8 deletions src/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,42 @@ const cleanSettings = stateObj => {
return nextState;
};

export function writeHash(mapSettings) {
export function createHashString(mapSettings) {
let newMapSettings = JSON.parse(JSON.stringify(mapSettings));
if (newMapSettings.maps?.length ?? 0) {
const newMaps = newMapSettings.maps;

// Remove map styles before hashing
newMapSettings.maps = newMaps.map(m => {
delete m.style;
return m;
});
}

let nonMapSettings = Object.fromEntries(
Object.entries(mapSettings)
Object.entries(newMapSettings)
.filter(([k, v]) => !mapLocationKeys.includes(k))
.map(([k, v]) => [k, jsonKeys.includes(k) ? JSON.stringify(v) : v])
);

nonMapSettings = cleanSettings(nonMapSettings);

window.location.hash = toQueryString({
return toQueryString({
map: [
round(mapSettings.zoom, 2),
round(mapSettings.center.lat, 4),
round(mapSettings.center.lng, 4),
round(mapSettings.pitch, 1),
round(mapSettings.bearing, 1),
round(newMapSettings.zoom, 2),
round(newMapSettings.center.lat, 4),
round(newMapSettings.center.lng, 4),
round(newMapSettings.pitch, 1),
round(newMapSettings.bearing, 1),
].join('/'),
...nonMapSettings,
});
}

export function writeHash(mapSettings) {
window.location.hash = createHashString(mapSettings);
}

export function readHash(qs) {
// Remove unset values, convert value as necessary
let urlState = Object.fromEntries(
Expand Down
2 changes: 1 addition & 1 deletion src/settings.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readHash } from './query';

export const getInitialSettings = config => {
export const getSettings = config => {
const { mapState, maps, viewMode, stylePresets } = config;
return {
...mapState,
Expand Down

0 comments on commit 2d7435a

Please sign in to comment.