Add date-range filters to map
This commit is contained in:
parent
c4b9cbb607
commit
115e871ee9
|
@ -1,10 +1,6 @@
|
||||||
from gzip import decompress
|
from gzip import decompress
|
||||||
from sqlite3 import connect
|
from sqlite3 import connect
|
||||||
from datetime import datetime, time, timedelta
|
from sanic.exceptions import Forbidden
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
import dateutil.parser
|
|
||||||
from sanic.exceptions import Forbidden, InvalidUsage
|
|
||||||
from sanic.response import raw
|
from sanic.response import raw
|
||||||
|
|
||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, text
|
||||||
|
@ -93,6 +89,10 @@ async def tiles(req, zoom: int, x: int, y: str):
|
||||||
else:
|
else:
|
||||||
user_id, start, end = get_filter_options(req)
|
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(
|
tile = await req.ctx.db.scalar(
|
||||||
text(
|
text(
|
||||||
f"select data from getmvt(:zoom, :x, :y, :user_id, :min_time, :max_time) as b(data, key);"
|
f"select data from getmvt(:zoom, :x, :y, :user_id, :min_time, :max_time) as b(data, key);"
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
Header,
|
Header,
|
||||||
} from "semantic-ui-react";
|
} from "semantic-ui-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -33,6 +32,22 @@ const ROAD_ATTRIBUTE_OPTIONS = [
|
||||||
{value: 'usage_count', key: 'usage_count', text: 'Usage count'},
|
{value: 'usage_count', key: 'usage_count', text: 'Usage count'},
|
||||||
{value: 'zone', key: 'zone', text: 'Overtaking distance zone'}
|
{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({
|
function LayerSidebar({
|
||||||
mapConfig,
|
mapConfig,
|
||||||
|
@ -45,11 +60,11 @@ function LayerSidebar({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
baseMap: {style},
|
baseMap: { style },
|
||||||
obsRoads: {show: showRoads, showUntagged, attribute, maxCount},
|
obsRoads: { show: showRoads, showUntagged, attribute, maxCount },
|
||||||
obsEvents: {show: showEvents},
|
obsEvents: { show: showEvents },
|
||||||
obsRegions: {show: showRegions},
|
obsRegions: {show: showRegions},
|
||||||
filters: {
|
filters: {
|
||||||
currentUser: filtersCurrentUser,
|
currentUser: filtersCurrentUser,
|
||||||
dateMode,
|
dateMode,
|
||||||
startDate,
|
startDate,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useCallback, useMemo, useRef } from "react";
|
import React, { useState, useCallback, useMemo , useRef } from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { connect } from "react-redux";
|
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 { Layer, Source } from "react-map-gl";
|
||||||
import produce from "immer";
|
import produce from "immer";
|
||||||
|
|
||||||
|
@ -11,38 +11,38 @@ import {useConfig} from 'config'
|
||||||
import {colorByDistance, colorByCount, borderByZone, reds, isValidAttribute, getRegionLayers} from 'mapstyles'
|
import {colorByDistance, colorByCount, borderByZone, reds, isValidAttribute, getRegionLayers} from 'mapstyles'
|
||||||
import {useMapConfig} from 'reducers/mapConfig'
|
import {useMapConfig} from 'reducers/mapConfig'
|
||||||
|
|
||||||
import RoadInfo from './RoadInfo'
|
import RoadInfo from "./RoadInfo";
|
||||||
import RegionInfo from "./RegionInfo";
|
import RegionInfo from "./RegionInfo";
|
||||||
import LayerSidebar from './LayerSidebar'
|
import LayerSidebar from "./LayerSidebar";
|
||||||
import styles from './styles.module.less'
|
import styles from "./styles.module.less";
|
||||||
|
|
||||||
const untaggedRoadsLayer = {
|
const untaggedRoadsLayer = {
|
||||||
id: 'obs_roads_untagged',
|
id: "obs_roads_untagged",
|
||||||
type: 'line',
|
type: "line",
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_roads',
|
"source-layer": "obs_roads",
|
||||||
minzoom: 12,
|
minzoom: 12,
|
||||||
filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]],
|
filter: ["!", ["to-boolean", ["get", "distance_overtaker_mean"]]],
|
||||||
layout: {
|
layout: {
|
||||||
'line-cap': 'round',
|
"line-cap": "round",
|
||||||
'line-join': 'round',
|
"line-join": "round",
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2],
|
"line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 2, 17, 2],
|
||||||
'line-color': '#ABC',
|
"line-color": "#ABC",
|
||||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1],
|
"line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
|
||||||
'line-offset': [
|
"line-offset": [
|
||||||
'interpolate',
|
"interpolate",
|
||||||
['exponential', 1.5],
|
["exponential", 1.5],
|
||||||
['zoom'],
|
["zoom"],
|
||||||
12,
|
12,
|
||||||
['get', 'offset_direction'],
|
["get", "offset_direction"],
|
||||||
19,
|
19,
|
||||||
['*', ['get', 'offset_direction'], 8],
|
["*", ["get", "offset_direction"], 8],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
minzoom: 12,
|
minzoom: 12,
|
||||||
}
|
};
|
||||||
|
|
||||||
const getUntaggedRoadsLayer = (colorAttribute, maxCount) =>
|
const getUntaggedRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
produce(untaggedRoadsLayer, (draft) => {
|
produce(untaggedRoadsLayer, (draft) => {
|
||||||
|
@ -57,7 +57,7 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
draft.paint["line-width"][6] = 6; // scale bigger on zoom
|
draft.paint["line-width"][6] = 6; // scale bigger on zoom
|
||||||
draft.paint["line-color"] = colorAttribute.startsWith("distance_")
|
draft.paint["line-color"] = colorAttribute.startsWith("distance_")
|
||||||
? colorByDistance(colorAttribute)
|
? colorByDistance(colorAttribute)
|
||||||
: colorAttribute.endsWith('_count')
|
: colorAttribute.endsWith("_count")
|
||||||
? colorByCount(colorAttribute, maxCount)
|
? colorByCount(colorAttribute, maxCount)
|
||||||
: colorAttribute.endsWith("zone")
|
: colorAttribute.endsWith("zone")
|
||||||
? borderByZone()
|
? borderByZone()
|
||||||
|
@ -67,42 +67,42 @@ const getRoadsLayer = (colorAttribute, maxCount) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
const getEventsLayer = () => ({
|
const getEventsLayer = () => ({
|
||||||
id: 'obs_events',
|
id: "obs_events",
|
||||||
type: 'circle',
|
type: "circle",
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_events',
|
"source-layer": "obs_events",
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8],
|
"circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 3, 17, 8],
|
||||||
'circle-color': colorByDistance('distance_overtaker'),
|
"circle-color": colorByDistance("distance_overtaker"),
|
||||||
},
|
},
|
||||||
minzoom: 11,
|
minzoom: 11,
|
||||||
})
|
});
|
||||||
|
|
||||||
const getEventsTextLayer = () => ({
|
const getEventsTextLayer = () => ({
|
||||||
id: 'obs_events_text',
|
id: "obs_events_text",
|
||||||
type: 'symbol',
|
type: "symbol",
|
||||||
minzoom: 18,
|
minzoom: 18,
|
||||||
source: 'obs',
|
source: "obs",
|
||||||
'source-layer': 'obs_events',
|
"source-layer": "obs_events",
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': [
|
"text-field": [
|
||||||
'number-format',
|
"number-format",
|
||||||
['get', 'distance_overtaker'],
|
["get", "distance_overtaker"],
|
||||||
{'min-fraction-digits': 2, 'max-fraction-digits': 2},
|
{ "min-fraction-digits": 2, "max-fraction-digits": 2 },
|
||||||
],
|
],
|
||||||
'text-allow-overlap': true,
|
"text-allow-overlap": true,
|
||||||
'text-font': ['Open Sans Bold', 'Arial Unicode MS Regular'],
|
"text-font": ["Open Sans Bold", "Arial Unicode MS Regular"],
|
||||||
'text-size': 14,
|
"text-size": 14,
|
||||||
'text-keep-upright': false,
|
"text-keep-upright": false,
|
||||||
'text-anchor': 'left',
|
"text-anchor": "left",
|
||||||
'text-radial-offset': 1,
|
"text-radial-offset": 1,
|
||||||
'text-rotate': ['-', 90, ['*', ['get', 'course'], 180 / Math.PI]],
|
"text-rotate": ["-", 90, ["*", ["get", "course"], 180 / Math.PI]],
|
||||||
'text-rotation-alignment': 'map',
|
"text-rotation-alignment": "map",
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-halo-color': 'rgba(255, 255, 255, 1)',
|
"text-halo-color": "rgba(255, 255, 255, 1)",
|
||||||
'text-halo-width': 1,
|
"text-halo-width": 1,
|
||||||
'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1],
|
"text-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.3, 1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,15 +150,15 @@ type Details =
|
||||||
| { type: "road"; road: Object }
|
| { type: "road"; road: Object }
|
||||||
| { type: "region"; region: RegionInfo };
|
| { type: "region"; region: RegionInfo };
|
||||||
|
|
||||||
|
|
||||||
function MapPage({ login }) {
|
function MapPage({ login }) {
|
||||||
const {obsMapSource, banner } = useConfig() || {}
|
const { obsMapSource , banner } = useConfig() || {};
|
||||||
const [details, setDetails] = useState<null | Details>(null);
|
const [details, setDetails] = useState<null | Details>(null);
|
||||||
|
|
||||||
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
|
const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null)
|
||||||
|
|
||||||
const onCloseDetails = useCallback(() => setDetails(null), [setDetails]);
|
const onCloseDetails = useCallback(() => setDetails(null), [setDetails]);
|
||||||
|
|
||||||
|
|
||||||
const mapConfig = useMapConfig();
|
const mapConfig = useMapConfig();
|
||||||
|
|
||||||
const viewportRef = useRef();
|
const viewportRef = useRef();
|
||||||
|
@ -180,9 +180,9 @@ function MapPage({ login }) {
|
||||||
node?.classList?.contains(className)
|
node?.classList?.contains(className)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
node = node.parentNode
|
node = node.parentNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]})
|
setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]})
|
||||||
|
@ -208,13 +208,13 @@ function MapPage({ login }) {
|
||||||
setClickLocation(null);
|
setClickLocation(null);
|
||||||
}, [setClickLocation]);
|
}, [setClickLocation]);
|
||||||
|
|
||||||
const [layerSidebar, setLayerSidebar] = useState(true)
|
const [layerSidebar, setLayerSidebar] = useState(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
obsRoads: {attribute, maxCount},
|
obsRoads: { attribute, maxCount },
|
||||||
} = mapConfig
|
} = mapConfig;
|
||||||
|
|
||||||
const layers = []
|
const layers = [];
|
||||||
|
|
||||||
const untaggedRoadsLayerCustom = useMemo(
|
const untaggedRoadsLayerCustom = useMemo(
|
||||||
() => getUntaggedRoadsLayer(attribute),
|
() => getUntaggedRoadsLayer(attribute),
|
||||||
|
@ -224,7 +224,10 @@ function MapPage({ login }) {
|
||||||
layers.push(untaggedRoadsLayerCustom)
|
layers.push(untaggedRoadsLayerCustom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount])
|
const roadsLayer = useMemo(
|
||||||
|
() => getRoadsLayer(attribute, maxCount),
|
||||||
|
[attribute, maxCount]
|
||||||
|
);
|
||||||
if (mapConfig.obsRoads.show) {
|
if (mapConfig.obsRoads.show) {
|
||||||
layers.push(roadsLayer);
|
layers.push(roadsLayer);
|
||||||
}
|
}
|
||||||
|
@ -237,8 +240,8 @@ function MapPage({ login }) {
|
||||||
const eventsLayer = useMemo(() => getEventsLayer(), [])
|
const eventsLayer = useMemo(() => getEventsLayer(), [])
|
||||||
const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
|
const eventsTextLayer = useMemo(() => getEventsTextLayer(), [])
|
||||||
if (mapConfig.obsEvents.show) {
|
if (mapConfig.obsEvents.show) {
|
||||||
layers.push(eventsLayer)
|
layers.push(eventsLayer);
|
||||||
layers.push(eventsTextLayer)
|
layers.push(eventsTextLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onToggleLayerSidebarButtonClick = useCallback(
|
const onToggleLayerSidebarButtonClick = useCallback(
|
||||||
|
@ -252,9 +255,35 @@ function MapPage({ login }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!obsMapSource) {
|
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 tiles = obsMapSource?.tiles?.map((tileUrl: string) => {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (login) {
|
if (login) {
|
||||||
|
@ -303,7 +332,7 @@ function MapPage({ login }) {
|
||||||
<Map viewportFromUrl onClick={onClick} onViewportChange={onViewportChange}hasToolbar>
|
<Map viewportFromUrl onClick={onClick} onViewportChange={onViewportChange}hasToolbar>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
left: 44,
|
left: 44,
|
||||||
top: 9,
|
top: 9,
|
||||||
}}
|
}}
|
||||||
|
@ -333,12 +362,12 @@ function MapPage({ login }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<RoadInfo {...{clickLocation}} />
|
<RoadInfo {...{ clickLocation }} />
|
||||||
</Map>
|
</Map>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((state) => ({ login: state.login }))(MapPage);
|
export default connect((state) => ({ login: state.login }))(MapPage);
|
||||||
|
|
|
@ -29,6 +29,10 @@ export type MapConfig = {
|
||||||
};
|
};
|
||||||
filters: {
|
filters: {
|
||||||
currentUser: boolean;
|
currentUser: boolean;
|
||||||
|
dateMode: "none" | "range" | "threshold";
|
||||||
|
startDate?: null | string;
|
||||||
|
endDate?: null | string;
|
||||||
|
thresholdAfter?: null | boolean;
|
||||||
};
|
};
|
||||||
obsRegions: {
|
obsRegions: {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
@ -57,6 +61,10 @@ export const initialState: MapConfig = {
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
currentUser: false,
|
currentUser: false,
|
||||||
|
dateMode: "none",
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
thresholdAfter: true,
|
||||||
},
|
},
|
||||||
obsRegions: {
|
obsRegions: {
|
||||||
show: true,
|
show: true,
|
||||||
|
|
Loading…
Reference in a new issue