Add date-range filters to map

This commit is contained in:
Paul Bienkowski 2022-07-22 13:29:51 +02:00 committed by gluap
parent c4b9cbb607
commit 115e871ee9
No known key found for this signature in database
4 changed files with 126 additions and 74 deletions

View file

@ -1,10 +1,6 @@
from gzip import decompress
from sqlite3 import connect
from datetime import datetime, time, timedelta
from typing import Optional, Tuple
import dateutil.parser
from sanic.exceptions import Forbidden, InvalidUsage
from sanic.exceptions import Forbidden
from sanic.response import raw
from sqlalchemy import select, text
@ -93,6 +89,10 @@ async def tiles(req, zoom: int, x: int, y: str):
else:
user_id, start, end = get_filter_options(req)
parse_date = lambda s: dateutil.parser.parse(s)
start = req.ctx.get_single_arg("start", default=None, convert=parse_date)
end = req.ctx.get_single_arg("end", default=None, convert=parse_date)
tile = await req.ctx.db.scalar(
text(
f"select data from getmvt(:zoom, :x, :y, :user_id, :min_time, :max_time) as b(data, key);"

View file

@ -11,7 +11,6 @@ import {
Header,
} from "semantic-ui-react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import {
@ -33,6 +32,22 @@ const ROAD_ATTRIBUTE_OPTIONS = [
{value: 'usage_count', key: 'usage_count', text: 'Usage count'},
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'}
]
"distance_overtaker_mean",
"distance_overtaker_min",
"distance_overtaker_max",
"distance_overtaker_median",
"overtaking_event_count",
"usage_count",
"zone",
];
const DATE_FILTER_MODES = [
{ value: "none", key: "none", text: "All time" },
{ value: "range", key: "range", text: "Start and end range" },
{ value: "threshold", key: "threshold", text: "Before/after comparison" },
];
type User = Object;
function LayerSidebar({
mapConfig,
@ -45,11 +60,11 @@ function LayerSidebar({
}) {
const { t } = useTranslation();
const {
baseMap: {style},
obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
obsEvents: {show: showEvents},
baseMap: { style },
obsRoads: { show: showRoads, showUntagged, attribute, maxCount },
obsEvents: { show: showEvents },
obsRegions: {show: showRegions},
filters: {
filters: {
currentUser: filtersCurrentUser,
dateMode,
startDate,

View file

@ -1,7 +1,7 @@
import React, { useState, useCallback, useMemo, useRef } from "react";
import React, { useState, useCallback, useMemo , useRef } from "react";
import _ from "lodash";
import { connect } from "react-redux";
import {Button } from "semantic-ui-react";
import { Button } from "semantic-ui-react";
import { Layer, Source } from "react-map-gl";
import produce from "immer";
@ -11,38 +11,38 @@ import {useConfig} from 'config'
import {colorByDistance, colorByCount, borderByZone, reds, isValidAttribute, getRegionLayers} from 'mapstyles'
import {useMapConfig} from 'reducers/mapConfig'
import RoadInfo from './RoadInfo'
import RoadInfo from "./RoadInfo";
import RegionInfo from "./RegionInfo";
import LayerSidebar from './LayerSidebar'
import styles from './styles.module.less'
import LayerSidebar from "./LayerSidebar";
import styles from "./styles.module.less";
const untaggedRoadsLayer = {
id: 'obs_roads_untagged',
type: 'line',
source: 'obs',
'source-layer': 'obs_roads',
id: "obs_roads_untagged",
type: "line",
source: "obs",
"source-layer": "obs_roads",
minzoom: 12,
filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]],
filter: ["!", ["to-boolean", ["get", "distance_overtaker_mean"]]],
layout: {
'line-cap': 'round',
'line-join': 'round',
"line-cap": "round",
"line-join": "round",
},
paint: {
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
'line-color': '#ABC',
'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1],
'line-offset': [
'interpolate',
['exponential', 1.5],
['zoom'],
"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 2, 17, 2],
"line-color": "#ABC",
"line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
"line-offset": [
"interpolate",
["exponential", 1.5],
["zoom"],
12,
['get', 'offset_direction'],
["get", "offset_direction"],
19,
['*', ['get', 'offset_direction'], 8],
["*", ["get", "offset_direction"], 8],
],
},
minzoom: 12,
}
};
const getUntaggedRoadsLayer = (colorAttribute, maxCount) =>
produce(untaggedRoadsLayer, (draft) => {
@ -57,7 +57,7 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
draft.paint["line-width"][6] = 6; // scale bigger on zoom
draft.paint["line-color"] = colorAttribute.startsWith("distance_")
? colorByDistance(colorAttribute)
: colorAttribute.endsWith('_count')
: colorAttribute.endsWith("_count")
? colorByCount(colorAttribute, maxCount)
: colorAttribute.endsWith("zone")
? borderByZone()
@ -67,42 +67,42 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
});
const getEventsLayer = () => ({
id: 'obs_events',
type: 'circle',
source: 'obs',
'source-layer': 'obs_events',
id: "obs_events",
type: "circle",
source: "obs",
"source-layer": "obs_events",
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8],
'circle-color': colorByDistance('distance_overtaker'),
"circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 3, 17, 8],
"circle-color": colorByDistance("distance_overtaker"),
},
minzoom: 11,
})
});
const getEventsTextLayer = () => ({
id: 'obs_events_text',
type: 'symbol',
id: "obs_events_text",
type: "symbol",
minzoom: 18,
source: 'obs',
'source-layer': 'obs_events',
source: "obs",
"source-layer": "obs_events",
layout: {
'text-field': [
'number-format',
['get', 'distance_overtaker'],
{'min-fraction-digits': 2, 'max-fraction-digits': 2},
"text-field": [
"number-format",
["get", "distance_overtaker"],
{ "min-fraction-digits": 2, "max-fraction-digits": 2 },
],
'text-allow-overlap': true,
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
'text-size': 14,
'text-keep-upright': false,
'text-anchor': 'left',
'text-radial-offset': 1,
'text-rotate': ['-', 90, ['*', ['get', 'course'], 180 / Math.PI]],
'text-rotation-alignment': 'map',
"text-allow-overlap": true,
"text-font": ["Open Sans Bold", "Arial Unicode MS Regular"],
"text-size": 14,
"text-keep-upright": false,
"text-anchor": "left",
"text-radial-offset": 1,
"text-rotate": ["-", 90, ["*", ["get", "course"], 180 / Math.PI]],
"text-rotation-alignment": "map",
},
paint: {
'text-halo-color': 'rgba(255, 255, 255, 1)',
'text-halo-width': 1,
'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1],
"text-halo-color": "rgba(255, 255, 255, 1)",
"text-halo-width": 1,
"text-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.3, 1],
},
});
@ -150,15 +150,15 @@ type Details =
| { type: "road"; road: Object }
| { type: "region"; region: RegionInfo };
function MapPage({ login }) {
const {obsMapSource, banner } = useConfig() || {}
const { obsMapSource , banner } = useConfig() || {};
const [details, setDetails] = useState<null | Details>(null);
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
const onCloseDetails = useCallback(() => setDetails(null), [setDetails]);
const mapConfig = useMapConfig();
const viewportRef = useRef();
@ -180,9 +180,9 @@ function MapPage({ login }) {
node?.classList?.contains(className)
)
) {
return
return;
}
node = node.parentNode
node = node.parentNode;
}
setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]})
@ -208,13 +208,13 @@ function MapPage({ login }) {
setClickLocation(null);
}, [setClickLocation]);
const [layerSidebar, setLayerSidebar] = useState(true)
const [layerSidebar, setLayerSidebar] = useState(true);
const {
obsRoads: {attribute, maxCount},
} = mapConfig
obsRoads: { attribute, maxCount },
} = mapConfig;
const layers = []
const layers = [];
const untaggedRoadsLayerCustom = useMemo(
() => getUntaggedRoadsLayer(attribute),
@ -224,7 +224,10 @@ function MapPage({ login }) {
layers.push(untaggedRoadsLayerCustom)
}
const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount])
const roadsLayer = useMemo(
() => getRoadsLayer(attribute, maxCount),
[attribute, maxCount]
);
if (mapConfig.obsRoads.show) {
layers.push(roadsLayer);
}
@ -237,8 +240,8 @@ function MapPage({ login }) {
const eventsLayer = useMemo(() => getEventsLayer(), [])
const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
if (mapConfig.obsEvents.show) {
layers.push(eventsLayer)
layers.push(eventsTextLayer)
layers.push(eventsLayer);
layers.push(eventsTextLayer);
}
const onToggleLayerSidebarButtonClick = useCallback(
@ -252,9 +255,35 @@ function MapPage({ login }) {
);
if (!obsMapSource) {
return null
return null;
}
const tiles = obsMapSource?.tiles?.map(
(tileUrl: string) => {
const query = new URLSearchParams()
if (login) {
if (mapConfig.filters.currentUser) {
query.append('user', login.username)
}
if (mapConfig.filters.dateMode === "range") {
if (mapConfig.filters.startDate) {
query.append('start', mapConfig.filters.startDate)
}
if (mapConfig.filters.endDate) {
query.append('end', mapConfig.filters.endDate)
}
} else if (mapConfig.filters.dateMode === "threshold") {
if (mapConfig.filters.startDate) {
query.append(mapConfig.filters.thresholdAfter ? 'start' : 'end', mapConfig.filters.startDate)
}
}
}
const queryString = String(query)
return tileUrl + (queryString ? '?' : '') + queryString
}
);
const tiles = obsMapSource?.tiles?.map((tileUrl: string) => {
const query = new URLSearchParams();
if (login) {
@ -303,7 +332,7 @@ function MapPage({ login }) {
<Map viewportFromUrl onClick={onClick} onViewportChange={onViewportChange}hasToolbar>
<Button
style={{
position: 'absolute',
position: "absolute",
left: 44,
top: 9,
}}
@ -333,12 +362,12 @@ function MapPage({ login }) {
/>
)}
<RoadInfo {...{clickLocation}} />
<RoadInfo {...{ clickLocation }} />
</Map>
</div>
</div>
</Page>
)
);
}
export default connect((state) => ({ login: state.login }))(MapPage);

View file

@ -29,6 +29,10 @@ export type MapConfig = {
};
filters: {
currentUser: boolean;
dateMode: "none" | "range" | "threshold";
startDate?: null | string;
endDate?: null | string;
thresholdAfter?: null | boolean;
};
obsRegions: {
show: boolean;
@ -57,6 +61,10 @@ export const initialState: MapConfig = {
},
filters: {
currentUser: false,
dateMode: "none",
startDate: null,
endDate: null,
thresholdAfter: true,
},
obsRegions: {
show: true,