Skip to content

Commit

Permalink
towards addressing #27 and hopefully #30
Browse files Browse the repository at this point in the history
  • Loading branch information
carlhiggs committed Oct 1, 2024
1 parent c5671cc commit 3f752fd
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 87 deletions.
2 changes: 1 addition & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="manifest.json" />
<link rel="manifest" href="/manifest.json" />
<title>Transport & Health Impacts</title>
</head>
<body>
Expand Down
30 changes: 15 additions & 15 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Navbar from './components/navbar';
import './App.css';
import { Amplify } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { useLocation } from "react-router-dom";
import '@aws-amplify/ui-react/styles.css';
import awsconfig from '../amplify_outputs.json';
import Error404 from "./components/404-page";
Expand Down Expand Up @@ -52,16 +52,13 @@ const theme = createTheme({


export function useScrollToAnchor() {
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const current_location = window.location;
const navigate = useNavigate();
const location = useLocation();

useEffect(() => {
if (current_location.hash === '') window.scrollTo(0, 0)
if (location.hash === '') window.scrollTo(0, 0)
else {
setTimeout(() => {
const id = current_location.hash.replace('#', '')
const id = location.hash.replace('#', '')
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({
Expand All @@ -72,21 +69,24 @@ export function useScrollToAnchor() {
}
}, 0)
}


let protocol = new Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
return () => {
maplibregl.removeProtocol("pmtiles");
console.log(location);
console.log(current_location.href);
console.log(searchParams.size!==0);
if (current_location.pathname !== '/map' && searchParams.size!==0) {
setSearchParams([]);
console.log(searchParams);
}
// console.log(location);
// console.log(current_location.pathname+current_location.hash);
// if (!urlUpdated && location.pathname !== '/map' || current_location.pathname !== '/map') {
// // console.log('test');
// // Clear query strings by updating the URL without triggering a navigation
// const newUrl = `${location.pathname}${location.hash}`;
// window.history.replaceState(null, '', newUrl);
// setUrlUpdated(true);
// }
};

}, [current_location, location, navigate])
}, [location]);
}

const App: FC<AppProps> = () => {
Expand Down
15 changes: 12 additions & 3 deletions app/src/components/share.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import React from 'react';
import { Fab, Tooltip } from '@mui/material';
import ShareIcon from '@mui/icons-material/Share';
import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
import { FocusFeature} from './utilities';

export function ShareURL() {
interface ShareURLProps {
focusFeature?: FocusFeature;
}

export function ShareURL({ focusFeature }: ShareURLProps) {
const [open, setOpen] = React.useState(false);

const handleClick = () => {
navigator.clipboard.writeText(window.location.href);
console.log('Copied to clipboard:', window.location.href);
const baseUrl = `${window.location.origin}${window.location.pathname}`;
const queryString = focusFeature ? `?${focusFeature.getQueryString()}` : '';
const shareUrl = `${baseUrl}${queryString}`;

navigator.clipboard.writeText(shareUrl);
console.log('Copied to clipboard:', shareUrl);
setOpen(true);
};

Expand Down
33 changes: 33 additions & 0 deletions app/src/components/utilities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ export const capitalString = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
}

export class FocusFeature {
private features: { [key: string]: string };

constructor() {
this.features = {};
}

update(params: { [key: string]: string }) {
Object.entries(params).forEach(([queryKey, queryValue]) => {
const oldQuery = this.features[queryKey] ?? '';
if (queryValue === oldQuery) return;

if (queryValue && queryValue !== '') {
this.features[queryKey] = queryValue;
} else {
delete this.features[queryKey];
}
});
}

getAll() {
return this.features;
}

getQueryString() {
const queryParams = new URLSearchParams(this.features);
console.log(queryParams.toString())
return queryParams.toString();
}
}


export function updateSearchParams(params: { [key: string]: string }) {
const currentSearchParams = new URLSearchParams(window.location.search);
Expand All @@ -23,3 +54,5 @@ export function updateSearchParams(params: { [key: string]: string }) {
// Manually update the URL without triggering a navigation
window.history.replaceState(null, '', newUrl);
}


129 changes: 63 additions & 66 deletions app/src/components/vis/map/map.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FC, useRef, useEffect, useState, useCallback } from 'react';
import { FC, useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { updateSearchParams } from '../../utilities';
import { FocusFeature} from '../../utilities';
import cities from '../stories/cities.json';
import stories from '../stories/stories.json';
import maplibregl, { LngLatLike, MapMouseEvent, LayerSpecification as OriginalLayerSpecification } from 'maplibre-gl';
import maplibregl, { LngLatLike, MapMouseEvent } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
// import * as pmtiles from "pmtiles";
import layers from "protomaps-themes-base";
Expand All @@ -26,6 +26,7 @@ import { Steps, Hints } from 'intro.js-react';
import 'intro.js/introjs.css';
import { useSearchParams } from 'react-router-dom';
import ScenarioSettings from './map_scenario_settings';
import { ShareURL } from '../../share';

// const protocol = new pmtiles.Protocol();

Expand All @@ -44,37 +45,28 @@ interface MapProps {}

const Map: FC<MapProps> = (): JSX.Element => {
const [searchParams, _] = useSearchParams();
const [focusFeature, setFocusFeature] = useState(new FocusFeature());
const featureLoaded = useRef(false);

const scenario_setting = new ScenarioSettings();
scenario_setting.initialize(searchParams, stories, cities);


// const formatPopup = getFormatPopup(scenario.poup)
const initial_query = Object.fromEntries(searchParams.entries());
focusFeature.update(initial_query);

const scenario = scenario_setting.get();
console.log(scenario);
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<maplibregl.Map | null>(null);
const [lat] = useState<number>(Number(scenario['lat']));
const [lng] = useState<number>(Number(scenario['lng']));
const [zoom] = useState<number>(Number(scenario['zoom']));
let url_feature = {
source: searchParams.get('source'),
layer: searchParams.get('layer'),
id: searchParams.get('id'),
xy: searchParams.get('xy')?.split(',').map(parseFloat) as [number, number],
v: searchParams.get('v'),
filtered: searchParams.get('filter'),
}
const [featureLoaded, setFeatureLoaded] = useState<boolean>(false);
// Melbourne bbox
const [lat] = useState<number>(Number(initial_query['lat'] || scenario['lat']));
const [lng] = useState<number>(Number(initial_query['lng'] || scenario['lng']));
const [zoom] = useState<number>(Number(initial_query['zoom'] || scenario['zoom']));
const bounds = new maplibregl.LngLatBounds(scenario['bounds'] as unknown as LngLatLike);
let map_layers: string[] = [];

const handleMapClick = (e: MapMouseEvent, features:maplibregl.MapGeoJSONFeature[], scenario: { [key: string]: any }) => {
if (features[0] && 'id' in features[0]) {
// Add parameters to the URL query string
updateSearchParams({
focusFeature.update({
xy: e.lngLat.lng + ',' + e.lngLat.lat,
source: String(features[0]['source']) ?? '',
layer: String(features[0]['layer']['id']) ?? '',
Expand All @@ -86,7 +78,6 @@ const Map: FC<MapProps> = (): JSX.Element => {

useEffect(() => {
if (map.current) return; // stops map from intializing more than once

map.current = new maplibregl.Map({
container: mapContainer.current!,
style: {
Expand Down Expand Up @@ -166,7 +157,7 @@ const Map: FC<MapProps> = (): JSX.Element => {
}
}

updateSearchParams({'v': String(selectedVariable) ?? ''});
focusFeature.update({'v': String(selectedVariable) ?? ''});

});

Expand Down Expand Up @@ -196,14 +187,14 @@ const Map: FC<MapProps> = (): JSX.Element => {
}
});

updateSearchParams({'filter': className.replace('filtered-', '')});
focusFeature.update({'filter': className.replace('filtered-', '')});

} else if (className === 'unfiltered') {
layer_IDs.forEach((layer_ID: string) => {
map.current!.setFilter(layer_ID, null);
});

updateSearchParams({'filter':''});
focusFeature.update({'filter':''});

}
}
Expand All @@ -223,79 +214,85 @@ const Map: FC<MapProps> = (): JSX.Element => {
});
popup.on('close', () => {

updateSearchParams({'source':'','layer':'','id':''});
focusFeature.update({'source':'','layer':'','id':''});

})


function getFeatureFromURL() {
if (url_feature.v) {
const variableSelect = document.getElementById('variable-select') as HTMLSelectElement;
if (variableSelect && url_feature.v) {
variableSelect.value = url_feature.v;
variableSelect.dispatchEvent(new Event('change'));
}
}
const url_feature = Object.fromEntries(searchParams.entries());
// console.log('get feature: ', url_feature)
if (url_feature.v) {
const variableSelect = document.getElementById('variable-select') as HTMLSelectElement;
if (variableSelect && url_feature.v) {
variableSelect.value = url_feature.v;
variableSelect.dispatchEvent(new Event('change'));
}
}
if (url_feature.zoom) {
const new_zoom = parseFloat(url_feature.zoom)
map.current!.setZoom(new_zoom);
}

if (url_feature.source && url_feature.layer && url_feature.id && url_feature.xy) {
const features = map.current!.queryRenderedFeatures(
{ layers: [url_feature.layer] }
);
const feature = features!.find((feat) => String(feat.id) === url_feature.id);
if (feature) {
const scenario_layer = scenario.layers.find((x: { id: string; })=> x.id === feature.layer.id)
if ('popup' in scenario_layer) {
formatPopup(feature, url_feature.xy, map, popup, url_feature.layer);
setFeatureLoaded(true);
if (url_feature.source && url_feature.layer && url_feature.id && url_feature.xy) {
const features = map.current!.queryRenderedFeatures(
{ layers: [url_feature.layer] }
);
const feature = features!.find((feat) => String(feat.id) === url_feature.id);
if (feature) {
const scenario_layer = scenario.layers.find((x: { id: string; })=> x.id === feature.layer.id)
if ('popup' in scenario_layer) {
const xy = url_feature.xy.split(',').map(Number) as [number, number];
formatPopup(feature, xy, map, popup, url_feature.layer);
}
displayFeatureCheck(feature, scenario)
}
displayFeatureCheck(feature, scenario)
}
}
// if (url_feature.filtered && !featureLoaded) {
// const legendRow = document.getElementById('legend-row');
// if (legendRow) {
// // legendRow.className = 'filtered-' + url_feature.filtered;
// const filterCellIndex = url_feature.filtered.split('-')[0];
// const legendCell = document.getElementById(`legend-cell-${filterCellIndex}`);
// if (legendCell) {
// legendCell.click();
// }
// }
// }
setFeatureLoaded(true);
}
if (url_feature.filter && !featureLoaded.current) {
// console.log(url_feature.filter)
const legendRow = document.getElementById('legend-row');
if (legendRow) {
// legendRow.className = 'filtered-' + url_feature.filtered;
const filterCellIndex = url_feature.filter.split('-')[0];
const legendCell = document.getElementById(`legend-cell-${filterCellIndex}`);
if (legendCell) {
legendCell.click();
}
}
}
// window.history.replaceState({}, document.title, window.location.pathname);
};

map.current!.on('sourcedata', (e) => {
if (e.isSourceLoaded && !featureLoaded) {
if (e.isSourceLoaded && !featureLoaded.current) {
getFeatureFromURL()
featureLoaded.current = true;
}
});
});

map.current!.on('zoomend', function() {
const zoomLevel = Math.round(map.current!.getZoom() * 10) / 10;

updateSearchParams({'zoom': zoomLevel.toString()});

focusFeature.update({'zoom': zoomLevel.toString()});
setFocusFeature(focusFeature);
});
map.current!.on('moveend', function() {
const center = map.current!.getCenter();

updateSearchParams({'lat': center.lat.toFixed(4),'lng': center.lng.toFixed(4)});

focusFeature.update({'lat': center.lat.toFixed(4),'lng': center.lng.toFixed(4)});
setFocusFeature(focusFeature);
});


map.current.on('click', (e) => {
const features = map.current!.queryRenderedFeatures(e.point, { layers: map_layers });
handleMapClick(e, features, scenario);
});

}, [url_feature.filtered, featureLoaded, map, scenario, updateSearchParams]);
}, [featureLoaded, scenario]);

// console.log([lng, lat, zoom, url_feature, featureLoaded])
return (
<div className="map-wrap">
<ShareURL focusFeature={focusFeature} />
<Steps
enabled={true}
steps={scenario.steps}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/vis/map/map_scenario_settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ScenarioSettings {
this.scenario_settings.hints = this.scenario_settings.hints || [];
this.scenario_settings.layers = this.scenario_settings.layers || [];
this.scenario_settings.legend_layer = this.scenario_settings.legend_layer || 0;
console.log(this.scenario_settings.layers);
// console.log(this.scenario_settings.layers);

if (!this.scenario_settings.dictionary) {
if (this.scenario_settings.layers.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/vis/stories/stories.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"params": {
"city": "Melbourne",
"directions": "Select a road segment to view a range of metrics related to suitability for walking and cycling.",
"help": "<p>Level of Traffic Stress (LTS) for cycling along discrete road segments has been measured specifically for the Victorian policy context. The classification ranges from 1 (lowest stress, for use by all cyclists) to 4 (most stressful, and least suitable for safe cycling). Our implementation of this measure draws on research developed at RMIT by Dr Afshin Jafari and Steve Pemberton (<a href='https://github.com/rmit-astm/cycling-safety-comfort' target='_blank'>read more</a>).</p><p>Multiple variables may contribute to comfort or stress when cycling, including traffic intensity, intersection design, and presence of separated bike paths. However, environmental aspects such as greenery and shade are also factors influencing cycling choices.</p><p>Pemberton, S., & Jafari, A. (2024). Cycling safety and comfort (v1.0.1). Zenodo. <a href='https://doi.org/10.5281/zenodo.13831295' target='_blank'>https://doi.org/10.5281/zenodo.13831295</a><img src='images/cycling-traffic-stress-Jafari-Pemberton-2024.png' width='95%'></p>",
"help": "<p>Level of Traffic Stress (LTS) for cycling along discrete road segments has been measured specifically for the Victorian policy context. The classification ranges from 1 (lowest stress, for use by all cyclists) to 4 (most stressful, and least suitable for safe cycling). Our implementation of this measure draws on research developed at RMIT by Dr Afshin Jafari and Steve Pemberton (<a href='https://github.com/rmit-astm/cycling-safety-comfort' target='_blank'>read more</a>).</p><p>Multiple variables may contribute to comfort or stress when cycling, including traffic intensity, intersection design, and presence of separated bike paths. However, environmental aspects such as greenery and shade are also factors influencing cycling choices.</p><p>Pemberton, S., & Jafari, A. (2024). Cycling safety and comfort (v1.0.1). Zenodo. <a href='https://doi.org/10.5281/zenodo.13831295' target='_blank'>https://doi.org/10.5281/zenodo.13831295</a><img src='/images/cycling-traffic-stress-Jafari-Pemberton-2024.png' width='95%'></p>",
"legend_layer": 0,
"source": {
"network": {
Expand Down

0 comments on commit 3f752fd

Please sign in to comment.