From 115e871ee9353fe5a3794439de957ca085dde8da Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Fri, 22 Jul 2022 13:29:51 +0200 Subject: [PATCH] Add date-range filters to map --- api/obs/api/routes/tiles.py | 10 +- frontend/src/pages/MapPage/LayerSidebar.tsx | 25 +++- frontend/src/pages/MapPage/index.tsx | 157 ++++++++++++-------- frontend/src/reducers/mapConfig.ts | 8 + 4 files changed, 126 insertions(+), 74 deletions(-) diff --git a/api/obs/api/routes/tiles.py b/api/obs/api/routes/tiles.py index 9b6b652..3579687 100644 --- a/api/obs/api/routes/tiles.py +++ b/api/obs/api/routes/tiles.py @@ -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);" diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index b787fc5..42b7f1e 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -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, diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx index d3e6d2f..c5c78fc 100644 --- a/frontend/src/pages/MapPage/index.tsx +++ b/frontend/src/pages/MapPage/index.tsx @@ -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); 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 }) {