diff --git a/frontend/src/Components/Map/Hooks/useZoom.tsx b/frontend/src/Components/Map/Hooks/useZoom.tsx index 1700a5d5..36e8c8f9 100644 --- a/frontend/src/Components/Map/Hooks/useZoom.tsx +++ b/frontend/src/Components/Map/Hooks/useZoom.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react" import { useMapEvents } from "react-leaflet" - +//zoom implementation on map events const useZoom = () => { const [zoom, setZoom] = useState() diff --git a/frontend/src/Components/Map/MapWrapper.tsx b/frontend/src/Components/Map/MapWrapper.tsx index c77615ba..64a3c942 100644 --- a/frontend/src/Components/Map/MapWrapper.tsx +++ b/frontend/src/Components/Map/MapWrapper.tsx @@ -6,6 +6,7 @@ import Zoom from './Zoom'; import '../../css/map.css' import { MAP_OPTIONS } from './constants'; +//container for map and its attributes const MapWrapper = ( props : any ) => { const { children } = props; diff --git a/frontend/src/Components/Map/Renderers/DistHotline.tsx b/frontend/src/Components/Map/Renderers/DistHotline.tsx index 3241aeae..d520004c 100644 --- a/frontend/src/Components/Map/Renderers/DistHotline.tsx +++ b/frontend/src/Components/Map/Renderers/DistHotline.tsx @@ -1,5 +1,6 @@ + import { FC, useEffect, useMemo, useState } from 'react'; -import { LeafletEvent, Polyline } from 'leaflet'; +import { LeafletEvent, Polyline } from 'leaflet' import { HotlineOptions, useCustomHotline } from 'react-leaflet-hotline'; import { useGraph } from '../../../context/GraphContext'; @@ -9,87 +10,60 @@ import { Condition, Node, WayId } from '../../../models/path'; import DistRenderer from '../../../assets/hotline/DistRenderer'; import { DistData } from '../../../assets/hotline/hotline'; import HoverHotPolyline from '../../../assets/hotline/HoverHotPolyline'; -import { - HotlineEventFn, - HotlineEventHandlers, -} from 'react-leaflet-hotline/dist/types/types'; +import { HotlineEventFn, HotlineEventHandlers } from 'react-leaflet-hotline/lib/types'; import useZoom from '../Hooks/useZoom'; -import { useHoverContext } from '../../../context/GraphHoverContext'; +import { useHoverContext } from "../../../context/GraphHoverContext"; + const getLat = (n: Node) => n.lat; const getLng = (n: Node) => n.lng; const getVal = (n: Node) => n.way_dist; -const getWeight = (z: number | undefined) => - z === undefined ? 0 : Math.max(z > 8 ? z - 6 : z - 5, 2); +const getWeight = (z: number | undefined) => z === undefined ? 0 : Math.max(z > 8 ? z - 6 : z - 5, 2) interface IDistHotline { - way_ids: WayId[]; - geometry: Node[][]; - conditions: Condition[][]; - options?: HotlineOptions; - eventHandlers?: HotlineEventHandlers; + way_ids: WayId[]; + geometry: Node[][]; + conditions: Condition[][]; + options?: HotlineOptions, + eventHandlers?: HotlineEventHandlers; +} + +const handler = (eventHandlers: HotlineEventHandlers | undefined, event: keyof HotlineEventHandlers, opacity: number) => { + return (e: LeafletEvent, i: number, p: Polyline) => { + p.setStyle( { opacity } ) + if ( eventHandlers && eventHandlers[event] !== undefined ) + (eventHandlers[event] as HotlineEventFn)(e, i, p); + } +} + +const DistHotline: FC = ( { way_ids, geometry, conditions, options, eventHandlers } ) => { + + const { dotHover } = useHoverContext() + const zoom = useZoom() + + const opts = useMemo( () => ({ + ...options, weight: getWeight(zoom) + }), [options, zoom] ) + + const handlers: HotlineEventHandlers = useMemo( () => ({ + ...eventHandlers, + mouseover: handler(eventHandlers, 'mouseover', 0.5), + mouseout: handler(eventHandlers, 'mouseout', 0), + }), [eventHandlers] ) + + const { hotline } = useCustomHotline( + DistRenderer, HoverHotPolyline, + { data: geometry, getLat, getLng, getVal, options: opts, eventHandlers: handlers }, + way_ids, conditions + ); + + useEffect( () => { + if ( hotline === undefined ) return; + (hotline as HoverHotPolyline).setHover(dotHover) + }, [dotHover]) + + return null; } -const handler = ( - eventHandlers: HotlineEventHandlers | undefined, - event: keyof HotlineEventHandlers, - opacity: number, -) => { - return (e: LeafletEvent, i: number, p: Polyline) => { - p.setStyle({ opacity }); - if (eventHandlers && eventHandlers[event] !== undefined) - (eventHandlers[event] as HotlineEventFn)(e, i, p); - }; -}; - -const DistHotline: FC = ({ - way_ids, - geometry, - conditions, - options, - eventHandlers, -}) => { - const { dotHover } = useHoverContext(); - const zoom = useZoom(); - - const opts = useMemo( - () => ({ - ...options, - weight: getWeight(zoom), - }), - [options, zoom], - ); - - const handlers: HotlineEventHandlers = useMemo( - () => ({ - ...eventHandlers, - mouseover: handler(eventHandlers, 'mouseover', 0.5), - mouseout: handler(eventHandlers, 'mouseout', 0), - }), - [eventHandlers], - ); - - const { hotline } = useCustomHotline( - DistRenderer, - HoverHotPolyline, - { - data: geometry, - getLat, - getLng, - getVal, - options: opts, - eventHandlers: handlers, - }, - way_ids, - conditions, - ); - - useEffect(() => { - if (hotline === undefined) return; - (hotline as HoverHotPolyline).setHover(dotHover); - }, [dotHover]); - - return null; -}; - -export default DistHotline; + +export default DistHotline; \ No newline at end of file diff --git a/frontend/src/Components/Map/Zoom.tsx b/frontend/src/Components/Map/Zoom.tsx index b3f5c479..977eaac6 100644 --- a/frontend/src/Components/Map/Zoom.tsx +++ b/frontend/src/Components/Map/Zoom.tsx @@ -2,6 +2,7 @@ import { ZoomControl } from "react-leaflet" import useZoom from "./Hooks/useZoom" +// implement zoom component on map const Zoom = () => { const zoom = useZoom() diff --git a/frontend/src/Components/Palette/PaletteEditor.tsx b/frontend/src/Components/Palette/PaletteEditor.tsx index 3833f305..ab7f755c 100644 --- a/frontend/src/Components/Palette/PaletteEditor.tsx +++ b/frontend/src/Components/Palette/PaletteEditor.tsx @@ -1,47 +1,34 @@ -import { FC, MouseEvent, useState } from 'react'; -import { Gradient } from 'react-gradient-hook'; -import { CursorOptions } from 'react-gradient-hook/lib/types'; -import { useMap } from 'react-leaflet'; -import { Palette } from 'react-leaflet-hotline'; +import { FC, MouseEvent, useState } from "react"; +import { Gradient } from "react-gradient-hook"; +import { CursorOptions } from "react-gradient-hook/lib/types"; +import { useMap } from "react-leaflet"; +import { Palette } from "react-leaflet-hotline"; -import '../../css/palette.css'; +import '../../css/palette.css' interface IPaletteEditor { - width: number | undefined; - defaultPalette?: Palette; - cursorOptions?: CursorOptions; - onChange?: (palette: Palette) => void; + width: number | undefined; + defaultPalette?: Palette; + cursorOptions?: CursorOptions; + onChange?: (palette: Palette) => void; } -const PaletteEditor: FC = ({ - width, - defaultPalette, - cursorOptions, - onChange, -}) => { - const [show, setShow] = useState(false); - - const toggleAppear = () => setShow((prev) => !prev); - - if (width === undefined || width === 0) return null; - - return ( -
-
- -
-
- 🎨 -
-
- ); -}; +const PaletteEditor: FC = ( { width, defaultPalette, cursorOptions, onChange } ) => { + + const [show, setShow] = useState(false) + + const toggleAppear = () => setShow(prev => !prev) + + if ( width === undefined || width === 0 ) return null; + + return ( +
+
+ +
+
🎨
+
+ ) +} export default PaletteEditor; diff --git a/frontend/src/Components/RoadConditions/ConditionsGraph.tsx b/frontend/src/Components/RoadConditions/ConditionsGraph.tsx index 6f1dec46..ddf132d3 100644 --- a/frontend/src/Components/RoadConditions/ConditionsGraph.tsx +++ b/frontend/src/Components/RoadConditions/ConditionsGraph.tsx @@ -1,133 +1,97 @@ -import { FC, useCallback, useEffect, useMemo, useRef } from 'react'; -import { - ChartData, - Chart, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - ActiveElement, - ChartEvent, - ChartOptions, - ChartTypeRegistry, - Plugin, -} from 'chart.js'; -import { Color, Palette } from 'react-leaflet-hotline'; -import { Line } from 'react-chartjs-2'; +import { FC, useCallback, useEffect, useMemo, useRef } from "react"; +import { ChartData, Chart, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ActiveElement, ChartEvent, ChartOptions, ChartTypeRegistry, Plugin } from "chart.js"; +import { Color, Palette } from "react-leaflet-hotline"; +import { Line } from "react-chartjs-2"; -import { ConditionType } from '../../models/graph'; +import { ConditionType } from "../../models/graph"; -Chart.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, -); +Chart.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend ); -const options = ({ name, min, max }: ConditionType): ChartOptions<'line'> => ({ - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top' as const, - labels: { color: 'white' }, +const options = ({name, min, max}: ConditionType): ChartOptions<'line'> => ({ + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { color: 'white' }, + }, }, - }, - scales: { - x: { - title: { - display: true, - text: 'distance (m)', - }, - ticks: { - maxTicksLimit: 30, - stepSize: 200, - callback: (tick: string | number) => - Math.round(parseFloat(tick.toString())), - }, - }, - y: { - title: { - display: true, - text: name, - }, - min: min, - max: max, - }, - }, + scales: { + x: { + title: { + display: true, + text: 'distance (m)' + }, + ticks: { + maxTicksLimit: 30, + stepSize: 200, + callback: (tick: string | number) => Math.round(parseFloat(tick.toString())) + } + }, + y: { + title: { + display: true, + text: name + }, + min: min, + max: max + } + } }); interface Props { - type: ConditionType; - data: ChartData<'line', number[], number> | undefined; - palette: Palette; + type: ConditionType; + data: ChartData<"line", number[], number> | undefined + palette: Palette; } -const ConditionsGraph: FC = ({ type, data, palette }) => { - const ref = useRef>(null); +const ConditionsGraph: FC = ( { type, data, palette } ) => { - const addPaletteChart = - (palette: Palette) => - (chart: Chart) => { - const dataset = chart.data.datasets[0]; - const gradient = chart.ctx.createLinearGradient( - 0, - chart.chartArea.bottom, - 0, - 0, - ); - console.log(...palette); - palette.forEach((c: Color) => { - gradient.addColorStop(c.t, `rgb(${c.r}, ${c.g}, ${c.b})`); - }); - dataset.borderColor = gradient; - dataset.backgroundColor = gradient; - }; + const ref = useRef>(null) - useEffect(() => { - if (ref.current === null) return; - const chart = ref.current; - addPaletteChart(palette)(chart); - chart.update(); - }, [ref, data, palette]); + const addPaletteChart = (palette: Palette) => (chart: Chart) => { + const dataset = chart.data.datasets[0]; + const gradient = chart.ctx.createLinearGradient(0, chart.chartArea.bottom, 0, 0); + console.log(...palette); + palette.forEach( (c: Color) => { + gradient.addColorStop(c.t, `rgb(${c.r}, ${c.g}, ${c.b})`); + }) + dataset.borderColor = gradient; + dataset.backgroundColor = gradient; + } - // attach events to the graph options - const graphOptions: ChartOptions<'line'> = useMemo( - () => ({ - ...options(type), - onClick: ( - event: ChartEvent, - elts: ActiveElement[], - chart: Chart, - ) => { - if (elts.length === 0) return; - const elt = elts[0]; // doesnt work if multiple datasets - const pointIndex = elt.index; - console.log(pointIndex, event, elts); - }, - }), - [], - ); + useEffect( () => { + if (ref.current === null ) return; + const chart = ref.current; + addPaletteChart(palette)(chart) + chart.update() + }, [ref, data, palette]) - const plugins: Plugin<'line'>[] = [ - { - id: 'id', - }, - ]; + // attach events to the graph options + const graphOptions: ChartOptions<'line'> = useMemo( () => ({ + ...options(type), + onClick: (event: ChartEvent, elts: ActiveElement[], chart: Chart) => { + if ( elts.length === 0 ) return; + const elt = elts[0] // doesnt work if multiple datasets + const pointIndex = elt.index + console.log(pointIndex, event, elts); + } + }), [] ) - return ( -
- {data && ( - - )} -
- ); -}; + const plugins: Plugin<"line">[] = [ { + id: 'id', + } ] + + return ( +
+ { data && + } +
+ ) +} -export default ConditionsGraph; +export default ConditionsGraph; \ No newline at end of file diff --git a/frontend/src/Components/RoadConditions/ConditionsMap.tsx b/frontend/src/Components/RoadConditions/ConditionsMap.tsx index a3d9d35a..afad1c7f 100644 --- a/frontend/src/Components/RoadConditions/ConditionsMap.tsx +++ b/frontend/src/Components/RoadConditions/ConditionsMap.tsx @@ -23,6 +23,7 @@ interface Props { setWayData: React.Dispatch | undefined>>; } +//part of Road Conditions component const ConditionsMap: FC = ( { type, palette, setPalette, setWayData } ) => { const { name, max, grid, samples } = type; diff --git a/frontend/src/Components/RoadConditions/Ways.tsx b/frontend/src/Components/RoadConditions/Ways.tsx index 31cab719..7a4cd3ab 100644 --- a/frontend/src/Components/RoadConditions/Ways.tsx +++ b/frontend/src/Components/RoadConditions/Ways.tsx @@ -1,7 +1,8 @@ + import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { TRGB } from 'react-gradient-hook/lib/types'; import { HotlineOptions } from 'react-leaflet-hotline'; -import { HotlineEventHandlers } from 'react-leaflet-hotline/dist/types/types'; +import { HotlineEventHandlers } from 'react-leaflet-hotline/lib/types'; import { useGraph } from '../../context/GraphContext'; import { WaysConditions } from '../../models/path'; import { getWaysConditions } from '../../queries/conditions'; @@ -9,57 +10,51 @@ import useZoom from '../Map/Hooks/useZoom'; import DistHotline from '../Map/Renderers/DistHotline'; interface IWays { - palette: TRGB[]; - type: string; - onClick?: (way_id: string, way_length: number) => void; + palette: TRGB[] + type: string; + onClick?: (way_id: string, way_length: number) => void; } -const Ways: FC = ({ palette, type, onClick }) => { - const zoom = useZoom(); - const { minY, maxY } = useGraph(); - - const [ways, setWays] = useState(); - - const options = useMemo( - () => ({ - palette, - min: minY, - max: maxY, - }), - [palette, minY, maxY], - ); - - const handlers = useMemo( - () => ({ - click: (_, i) => { - if (ways && onClick) onClick(ways.way_ids[i], ways.way_lengths[i]); - }, - }), - [ways], - ); - - useEffect(() => { - if (zoom === undefined) return; - const z = Math.max(0, zoom - 12); - getWaysConditions(type, z, (data: WaysConditions) => { - console.log(data); - setWays(data); - }); - }, [zoom]); - - return ( - <> - {ways ? ( - - ) : null} - - ); -}; +const Ways: FC = ( { palette, type, onClick } ) => { + + const zoom = useZoom(); + const { minY, maxY } = useGraph() + + const [ways, setWays] = useState() + + const options = useMemo( () => ({ + palette, min: minY, max: maxY + } ), [palette, minY, maxY] ) + + const handlers = useMemo( () => ({ + click: (_, i) => { + if ( ways && onClick ) + onClick(ways.way_ids[i], ways.way_lengths[i]) + }, + }), [ways] ) + + useEffect( () => { + if ( zoom === undefined ) return; + const z = Math.max(0, zoom - 12) + getWaysConditions(type, z, (data: WaysConditions) => { + console.log(data) + setWays( data ) + } ) + }, [zoom] ) + + return ( + <> + { ways + ? + : null + } + + ) +} -export default Ways; +export default Ways; \ No newline at end of file diff --git a/frontend/src/assets/graph/types.ts b/frontend/src/assets/graph/types.ts index c66dbc8f..f8962a97 100644 --- a/frontend/src/assets/graph/types.ts +++ b/frontend/src/assets/graph/types.ts @@ -1,72 +1,67 @@ -import { Selection } from 'd3'; -import { FC } from 'react'; -import { Color } from 'react-leaflet-hotline'; -import { Bounds } from '../../models/path'; -// SVG -export type SVG = d3.Selection; -export type SVGLayer = d3.Selection; +import { Selection } from "d3" +import { FC } from "react" +import { Color } from "react-leaflet-hotline"; +import { Bounds } from "../../models/path"; + +// SVG +export type SVG = d3.Selection +export type SVGLayer = d3.Selection // Axis -export interface IAxis { - svg: SVG; - axis: Axis | undefined; - width: number; - height: number; - zoom: number; - absolute?: boolean; - time?: boolean; +export interface IAxis { + svg: SVG; + axis: Axis | undefined, + width: number; + height: number; + zoom: number; + absolute?: boolean; + time?: boolean; } export type ReactAxis = FC; -export type Axis = d3.ScaleLinear; -export type GraphAxis = [Axis, Axis]; +export type Axis = d3.ScaleLinear +export type GraphAxis = [Axis, Axis] // Data format export type GraphPoint = { - x: number; - y: number; - lat: number; - lng: number; -}; // [number, number] + x: number, + y: number, + lat: number, + lng: number } // [number, number] -export type GraphData = GraphPoint[]; +export type GraphData = GraphPoint[] export interface Plot { - data: GraphData; - bounds?: Bounds; - label: string; + data: GraphData + bounds?: Bounds; + label: string; } // Events export interface DotHover { - label: string; - point: GraphPoint; + label: string; + point: GraphPoint } // Options export interface PathOptions { - stroke?: string; - strokeWidth?: number; + stroke?: string; + strokeWidth?: number; } export interface DotsOptions { - radius?: number; - opacity?: number; - fill?: string; + radius?: number; + opacity?: number; + fill?: string; } // Palette - Gradient -export type Gradient = Selection< - SVGStopElement, - Color, - SVGLinearGradientElement, - unknown ->; +export type Gradient = Selection // MinMax -export type MinMax = [number, number]; -export type AddMinMaxFunc = (label: string, bounds: Required) => void; -export type RemMinMaxFunc = (label: string) => void; +export type MinMax = [number, number] +export type AddMinMaxFunc = (label: string, bounds: Required) => void +export type RemMinMaxFunc = (label: string) => void // Callback -export type D3Callback = (event: any, d: GraphPoint) => void; +export type D3Callback = (event: any, d: GraphPoint) => void \ No newline at end of file diff --git a/frontend/src/assets/hotline/DistRenderer.ts b/frontend/src/assets/hotline/DistRenderer.ts index 8b4fddba..6b20c6ab 100644 --- a/frontend/src/assets/hotline/DistRenderer.ts +++ b/frontend/src/assets/hotline/DistRenderer.ts @@ -1,196 +1,186 @@ + import { LatLng, Map } from 'leaflet'; import { HotlineOptions, Renderer } from 'react-leaflet-hotline'; -import { Condition, Node, WayId } from '../../models/path'; +import { Condition, Node, WayId } from "../../models/path"; import { DotHover } from '../graph/types'; -import { DistData, DistPoint } from './hotline'; -import Edge from './Edge'; +import { DistData, DistPoint } from "./hotline"; +import Edge from "./Edge"; + export default class DistRenderer extends Renderer { - way_ids: string[]; - conditions: Condition[][]; - edgess: Edge[][]; - dotHover: DotHover | undefined; - - constructor(options?: HotlineOptions, ...args: any[]) { - super({ ...options }); - this.way_ids = args[0][0]; - this.conditions = args[0][1]; - this.edgess = []; - this.dotHover = undefined; - } - - projectLatLngs( - _map: Map, - latlngs: LatLng[], - result: any, - projectedBounds: any, - ) { - const len = latlngs.length; - const ring: DistData = []; - for (let i = 0; i < len; i++) { - ring[i] = _map.latLngToLayerPoint(latlngs[i]) as any; - ring[i].i = i; - ring[i].way_dist = latlngs[i].alt || 0; - projectedBounds.extend(ring[i]); + + way_ids: string[]; + conditions: Condition[][]; + edgess: Edge[][]; + dotHover: DotHover | undefined; + + constructor( options?: HotlineOptions, ...args: any[] ) + { + super({...options}) + this.way_ids = args[0][0]; + this.conditions = args[0][1]; + this.edgess = []; + this.dotHover = undefined; } - result.push(ring); - } - - onProjected(): number { - this.updateEdges(); - return 0; - } - - _addWayColorGradient( - gradient: CanvasGradient, - edge: Edge, - dist: number, - way_id: string, - ): void { - const opacity = - this.dotHover !== undefined && this.dotHover.label !== way_id ? 0.3 : 1; - try { - gradient.addColorStop(dist, `rgba(${edge.get().join(',')},${opacity})`); - } catch {} - } - - /** - * Find the closest conditions around each edge (node for ways) and interpolate the color - */ - private updateEdges() { - let i = 0; - - const calcValue = (a: Condition, b: Condition, cur: DistPoint) => { - const A = 1 - (cur.way_dist - a.way_dist); - const B = 1 - (cur.way_dist - b.way_dist); - return (A * a.value + B * b.value) / (A + B); - }; - - const getValue = (d: DistPoint, conditions: Condition[]): number => { - if (d.way_dist <= 0) return conditions[0].value; - else if (d.way_dist >= 1 || i >= conditions.length) - return conditions[conditions.length - 1].value; - - while (conditions[i].way_dist <= d.way_dist && ++i < conditions.length) {} - - if (i === 0) return conditions[0].value; - else if (i >= conditions.length - 1) - return conditions[conditions.length - 1].value; - - return calcValue(conditions[i - 1], conditions[i], d); - }; - - this.edgess = this.projectedData.map((data, j) => { - i = 0; - return data.map((d) => { - const value = getValue(d, this.conditions[j]); - const rgb = this.getRGBForValue(value); - return new Edge(...rgb); - }); - }); - } - - setWayIds(way_ids: WayId[]) { - this.way_ids = way_ids; - } - - setConditions(conditions: Condition[][]) { - this.conditions = conditions; - this.updateEdges(); - } - - _drawHotline(): void { - const ctx = this._ctx; - if (ctx === undefined) return; - - const dataLength = this._data.length; - - for (let i = 0; i < dataLength; i++) { - const path = this._data[i]; - const edges = this.edgess[i]; - - const way_id = this.way_ids[i]; - const conditions = this.conditions[i]; - - for (let j = 1; j < path.length; j++) { - const start = path[j - 1]; - const end = path[j]; - - const gradient = this._addGradient(ctx, start, end, conditions, way_id); - - this._addWayColorGradient(gradient, edges[start.i], 0, way_id); - this._addWayColorGradient(gradient, edges[end.i], 1, way_id); - - this.drawGradient(ctx, gradient, way_id, start, end); - } + + projectLatLngs(_map: Map, latlngs: LatLng[], result: any, projectedBounds: any) + { + const len = latlngs.length; + const ring: DistData = []; + for (let i = 0; i < len; i++) + { + ring[i] = _map.latLngToLayerPoint(latlngs[i]) as any; + ring[i].i = i + ring[i].way_dist = latlngs[i].alt || 0 + projectedBounds.extend(ring[i]); + } + result.push(ring); } - } - - drawGradient( - ctx: CanvasRenderingContext2D, - gradient: CanvasGradient, - way_id: string, - pointStart: DistPoint, - pointEnd: DistPoint, - ) { - ctx.beginPath(); - const hoverWeight = - this.dotHover !== undefined && - this.dotHover.label === way_id && - pointStart.way_dist <= this.dotHover.point.x && - pointEnd.way_dist >= this.dotHover.point.x - ? 10 - : 0; - - ctx.lineWidth = this._options.weight + hoverWeight; - ctx.strokeStyle = gradient; - ctx.moveTo(pointStart.x, pointStart.y); - ctx.lineTo(pointEnd.x, pointEnd.y); - ctx.stroke(); - ctx.closePath(); - } - - _addGradient( - ctx: CanvasRenderingContext2D, - start: DistPoint, - end: DistPoint, - conditions: Condition[], - way_id: string, - ): CanvasGradient { - const gradient: CanvasGradient = ctx.createLinearGradient( - start.x, - start.y, - end.x, - end.y, - ); - this.computeGradient(gradient, start, end, conditions, way_id); - return gradient; - } - - computeGradient( - gradient: CanvasGradient, - pointStart: DistPoint, - pointEnd: DistPoint, - conditions: Condition[], - way_id: string, - ) { - const start_dist = pointStart.way_dist; - const end_dist = pointEnd.way_dist; - - if (start_dist === end_dist) return; - - for (let i = 0; i < conditions.length; i++) { - // const { dist: way_dist, value } = conditions[i] as any - const { way_dist, value } = conditions[i]; - - if (way_dist < start_dist) continue; - else if (way_dist > end_dist) return; - - const rgb = this.getRGBForValue(value); - const dist = (way_dist - start_dist) / (end_dist - start_dist); - - this._addWayColorGradient(gradient, new Edge(...rgb), dist, way_id); + + onProjected(): number { + this.updateEdges(); + return 0; + } + + _addWayColorGradient(gradient: CanvasGradient, edge: Edge, dist: number, way_id: string): void { + const opacity = this.dotHover !== undefined && this.dotHover.label !== way_id + ? 0.3 + : 1 + try{ + gradient.addColorStop(dist, `rgba(${edge.get().join(',')},${opacity})`); + } + catch + { + } + } + + /** + * Find the closest conditions around each edge (node for ways) and interpolate the color + */ + private updateEdges() + { + let i = 0 + + const calcValue = (a: Condition, b: Condition, cur: DistPoint ) => { + const A = 1 - (cur.way_dist - a.way_dist) + const B = 1 - (cur.way_dist - b.way_dist) + return (A * a.value + B * b.value) / (A + B) + } + + const getValue = (d: DistPoint, conditions: Condition[]): number => { + if ( d.way_dist <= 0 ) return conditions[0].value + else if ( d.way_dist >= 1 || i >= conditions.length ) return conditions[conditions.length - 1].value + + while ( conditions[i].way_dist <= d.way_dist && ++i < conditions.length ) {} + + if ( i === 0 ) return conditions[0].value + else if ( i >= conditions.length - 1 ) return conditions[conditions.length - 1].value + + return calcValue(conditions[i - 1], conditions[i], d) + } + + this.edgess = this.projectedData.map( (data, j) => { + i = 0; + return data.map( d => { + const value = getValue(d, this.conditions[j]) + const rgb = this.getRGBForValue( value ) + return new Edge( ...rgb ) + } ) + } ) + } + + setWayIds( way_ids: WayId[] ) + { + this.way_ids = way_ids; + } + + setConditions(conditions: Condition[][]) + { + this.conditions = conditions + this.updateEdges() + } + + _drawHotline(): void + { + const ctx = this._ctx; + if ( ctx === undefined ) return; + + const dataLength = this._data.length + + for (let i = 0; i < dataLength; i++) + { + const path = this._data[i]; + const edges = this.edgess[i] + + const way_id = this.way_ids[i]; + const conditions = this.conditions[i] + + for (let j = 1; j < path.length; j++) + { + const start = path[j - 1]; + const end = path[j]; + + const gradient = this._addGradient(ctx, start, end, conditions, way_id); + + this._addWayColorGradient(gradient, edges[start.i], 0, way_id) + this._addWayColorGradient(gradient, edges[end.i], 1, way_id) + + this.drawGradient(ctx, gradient, way_id, start, end) + } + } + } + + drawGradient( + ctx: CanvasRenderingContext2D, gradient: CanvasGradient, way_id: string, + pointStart: DistPoint, pointEnd: DistPoint + ) { + ctx.beginPath(); + const hoverWeight = this.dotHover !== undefined + && this.dotHover.label === way_id + && (pointStart.way_dist) <= this.dotHover.point.x + && (pointEnd.way_dist) >= this.dotHover.point.x + ? 10 + : 0 + + ctx.lineWidth = this._options.weight + hoverWeight + ctx.strokeStyle = gradient; + ctx.moveTo(pointStart.x, pointStart.y); + ctx.lineTo(pointEnd.x, pointEnd.y); + ctx.stroke(); + ctx.closePath() + } + + _addGradient( ctx: CanvasRenderingContext2D, start: DistPoint, end: DistPoint, conditions: Condition[], way_id: string ): CanvasGradient + { + + const gradient: CanvasGradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y); + this.computeGradient(gradient, start, end, conditions, way_id) + return gradient + } + + computeGradient(gradient: CanvasGradient, pointStart: DistPoint, pointEnd: DistPoint, conditions: Condition[], way_id: string ) + { + const start_dist = pointStart.way_dist + const end_dist = pointEnd.way_dist + + if ( start_dist === end_dist ) return; + + for ( let i = 0; i < conditions.length; i++ ) + { + // const { dist: way_dist, value } = conditions[i] as any + const { way_dist, value } = conditions[i] + + if ( way_dist < start_dist ) continue; + else if ( way_dist > end_dist ) return; + + const rgb = this.getRGBForValue(value); + const dist = (way_dist - start_dist) / (end_dist - start_dist) + + this._addWayColorGradient(gradient, new Edge(...rgb), dist, way_id) + } } - } } + + + diff --git a/frontend/src/assets/hotline/Edge.ts b/frontend/src/assets/hotline/Edge.ts index 51ddaa3f..8a360e98 100644 --- a/frontend/src/assets/hotline/Edge.ts +++ b/frontend/src/assets/hotline/Edge.ts @@ -1,27 +1,34 @@ + + export default class Edge { - r: number; - g: number; - b: number; + r: number; + g: number; + b: number; - constructor(r: number, g: number, b: number) { - this.r = r; - this.g = g; - this.b = b; - } + constructor(r: number, g: number, b: number) + { + this.r = r; + this.g = g; + this.b = b; + } - add(o: Edge): Edge { - return new Edge(this.r + o.r, this.g + o.g, this.b + o.b); - } + add(o: Edge): Edge + { + return new Edge(this.r + o.r, this.g + o.g, this.b + o.b ) + } - sub(o: Edge): Edge { - return new Edge(this.r - o.r, this.g - o.g, this.b - o.b); - } + sub(o: Edge): Edge + { + return new Edge(this.r - o.r, this.g - o.g, this.b - o.b ) + } - mul(fac: number): Edge { - return new Edge(this.r * fac, this.g * fac, this.b * fac); - } + mul(fac: number): Edge + { + return new Edge( this.r * fac, this.g * fac, this.b * fac ) + } - get(): [number, number, number] { - return [this.r, this.g, this.b]; - } -} + get(): [number, number, number] + { + return [this.r, this.g, this.b] + } +} \ No newline at end of file diff --git a/frontend/src/assets/hotline/HoverHotPolyline.ts b/frontend/src/assets/hotline/HoverHotPolyline.ts index 18f0bc74..483b418f 100644 --- a/frontend/src/assets/hotline/HoverHotPolyline.ts +++ b/frontend/src/assets/hotline/HoverHotPolyline.ts @@ -1,11 +1,14 @@ import { HotPolyline } from 'react-leaflet-hotline'; import { DotHover } from '../graph/types'; -export default class HoverHotPolyline extends HotPolyline { - setHover(dotHover: DotHover | undefined) { - if (this._canvas._hotline === undefined) return; - (this._canvas._hotline as any).dotHover = dotHover; - this._canvas._update(); - this.redraw(); - } -} + +export default class HoverHotPolyline extends HotPolyline +{ + setHover(dotHover: DotHover | undefined) + { + if ( this._canvas._hotline === undefined ) return; + (this._canvas._hotline as any).dotHover = dotHover + this._canvas._update() + this.redraw() + } +} \ No newline at end of file diff --git a/frontend/src/assets/hotline/hotline.d.ts b/frontend/src/assets/hotline/hotline.d.ts index f9c20498..23db3b99 100644 --- a/frontend/src/assets/hotline/hotline.d.ts +++ b/frontend/src/assets/hotline/hotline.d.ts @@ -1,8 +1,4 @@ -// DistHotline -export interface DistPoint { - x: number; - y: number; - i: number; - way_dist: number; -} -export type DistData = DistPoint[]; + +// DistHotline +export interface DistPoint { x: number, y: number, i: number, way_dist: number }; +export type DistData = DistPoint[] diff --git a/frontend/src/context/GraphContext.tsx b/frontend/src/context/GraphContext.tsx index 6f8e3674..393e7b2f 100644 --- a/frontend/src/context/GraphContext.tsx +++ b/frontend/src/context/GraphContext.tsx @@ -1,16 +1,20 @@ -import { createContext, useContext } from 'react'; +import { + createContext, + useContext, +} from "react"; + +import useMinMaxAxis from "../hooks/useMinMaxAxis"; +import { AddMinMaxFunc, RemMinMaxFunc } from "../assets/graph/types"; -import useMinMaxAxis from '../hooks/useMinMaxAxis'; -import { AddMinMaxFunc, RemMinMaxFunc } from '../assets/graph/types'; interface ContextProps { - minX: number; - maxX: number; - minY: number; - maxY: number; + minX: number; + maxX: number; + minY: number; + maxY: number; - addBounds: AddMinMaxFunc; - remBounds: RemMinMaxFunc; + addBounds: AddMinMaxFunc; + remBounds: RemMinMaxFunc; } const GraphContext = createContext({} as ContextProps); @@ -18,24 +22,21 @@ const GraphContext = createContext({} as ContextProps); // TODO: remove bounds / refactor? -> is it needed really? // TODO: generalize DotHover into an "Event State" (to support for more events at once) export const GraphProvider = ({ children }: any) => { - const { bounds, addBounds, remBounds } = useMinMaxAxis(); - - const { minX, maxX, minY, maxY } = bounds; - - return ( - - {children} - - ); + + const { bounds, addBounds, remBounds } = useMinMaxAxis() + + const { minX, maxX, minY, maxY } = bounds; + + return ( + + {children} + + ); }; -export const useGraph = () => useContext(GraphContext); +export const useGraph = () => useContext(GraphContext); \ No newline at end of file diff --git a/frontend/src/context/GraphHoverContext.tsx b/frontend/src/context/GraphHoverContext.tsx index df24ff21..a41310e5 100644 --- a/frontend/src/context/GraphHoverContext.tsx +++ b/frontend/src/context/GraphHoverContext.tsx @@ -1,41 +1,41 @@ import { - createContext, - Dispatch, - SetStateAction, - useContext, - useState, -} from 'react'; + createContext, + Dispatch, + SetStateAction, + useContext, + useState, +} from "react"; -import { Map } from 'leaflet'; +import { Map } from "leaflet" + +import useMinMaxAxis from "../hooks/useMinMaxAxis"; +import { AddMinMaxFunc, DotHover, RemMinMaxFunc } from "../assets/graph/types"; -import useMinMaxAxis from '../hooks/useMinMaxAxis'; -import { AddMinMaxFunc, DotHover, RemMinMaxFunc } from '../assets/graph/types'; interface HoverContextProps { - dotHover: DotHover | undefined; - setDotHover: Dispatch>; - map: Map | undefined; - setMap: Dispatch>; + dotHover: DotHover | undefined; + setDotHover: Dispatch>; + map: Map | undefined; + setMap: Dispatch> } const HoverContext = createContext({} as HoverContextProps); export const HoverProvider = ({ children }: any) => { - const [dotHover, setDotHover] = useState(); - const [map, setMap] = useState(); - - return ( - - {children} - - ); + + const [ dotHover, setDotHover ] = useState() + const [ map, setMap ] = useState() + + return ( + + {children} + + ); }; -export const useHoverContext = () => useContext(HoverContext); +export const useHoverContext = () => useContext(HoverContext); \ No newline at end of file diff --git a/frontend/src/hooks/useMinMax.tsx b/frontend/src/hooks/useMinMax.tsx index 5b1b85f8..8a23b636 100644 --- a/frontend/src/hooks/useMinMax.tsx +++ b/frontend/src/hooks/useMinMax.tsx @@ -1,42 +1,39 @@ -import { useEffect, useState } from 'react'; -import { MinMax } from '../assets/graph/types'; +import { useEffect, useState } from "react" +import { MinMax } from "../assets/graph/types"; -type History = { [key: string]: [number, number] }; +type History = {[key: string]: [number, number]} -const resetMinMax = [ - Number.MAX_SAFE_INTEGER, - Number.MIN_SAFE_INTEGER, -] as MinMax; +const resetMinMax = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER] as MinMax -const useMinMax = (defaultInterval: MinMax) => { - const [bounds, setBounds] = useState(defaultInterval); - const [history, setHistory] = useState({}); - const addInterval = (id: string, [nmin, nmax]: MinMax) => { - setHistory((prev) => ({ ...prev, [id]: [nmin, nmax] })); - }; +const useMinMax = ( defaultInterval: MinMax ) => { + const [bounds, setBounds] = useState(defaultInterval); + const [history, setHistory] = useState({}) - const remInterval = (id: string) => { - setHistory((prev) => { - const temp = { ...prev }; - delete temp[id]; - return temp; - }); - }; + const addInterval = ( id: string, [nmin, nmax]: MinMax ) => { + setHistory( prev => ({ ...prev, [id]: [nmin, nmax] }) ) + } - useEffect(() => { - if (Object.keys(history).length === 0) return setBounds(defaultInterval); + const remInterval = (id: string) => { + setHistory( prev => { + const temp = { ...prev }; + delete temp[id]; + return temp; + } ) + } - const newMinMax = Object.values(history).reduce( - ([accMin, accMax], [curMin, curMax]) => - [Math.min(accMin, curMin), Math.max(accMax, curMax)] as MinMax, - resetMinMax, - ); + useEffect( () => { + if ( Object.keys(history).length === 0 ) + return setBounds( defaultInterval ) - setBounds(newMinMax); - }, [history]); + const newMinMax = Object.values(history).reduce( ([accMin, accMax], [curMin, curMax]) => + [Math.min(accMin, curMin), Math.max(accMax, curMax)] as MinMax + , resetMinMax ) - return { bounds, addInterval, remInterval }; -}; + setBounds(newMinMax) + }, [history] ) -export default useMinMax; + return { bounds, addInterval, remInterval } +} + +export default useMinMax; \ No newline at end of file diff --git a/frontend/src/hooks/useMinMaxAxis.tsx b/frontend/src/hooks/useMinMaxAxis.tsx index 3c351941..da5f1817 100644 --- a/frontend/src/hooks/useMinMaxAxis.tsx +++ b/frontend/src/hooks/useMinMaxAxis.tsx @@ -1,40 +1,34 @@ -import { MinMax } from '../assets/graph/types'; -import { Bounds } from '../models/path'; -import useMinMax from './useMinMax'; -const defaultMinMax = [0, 10] as MinMax; +import { MinMax } from "../assets/graph/types"; +import { Bounds } from "../models/path"; +import useMinMax from "./useMinMax"; + +const defaultMinMax = [0, 10] as MinMax const useMinMaxAxis = () => { - const { - bounds: boundsX, - addInterval: addIntervalX, - remInterval: remIntervalX, - } = useMinMax(defaultMinMax); - const { - bounds: boundsY, - addInterval: addIntervalY, - remInterval: remIntervalY, - } = useMinMax(defaultMinMax); - - const addBounds = (id: string, bounds: Required) => { - const { minX, maxX, minY, maxY } = bounds; - addIntervalX(id, [minX, maxX]); - addIntervalY(id, [minY, maxY]); - }; - - const remBounds = (id: string) => { - remIntervalX(id); - remIntervalY(id); - }; - - const bounds = { - minX: boundsX[0], - maxX: boundsX[1], - minY: boundsY[0], - maxY: boundsY[1], - }; - - return { bounds, addBounds, remBounds }; -}; - -export default useMinMaxAxis; + + const { bounds: boundsX, addInterval: addIntervalX, remInterval: remIntervalX } = useMinMax(defaultMinMax) + const { bounds: boundsY, addInterval: addIntervalY, remInterval: remIntervalY } = useMinMax(defaultMinMax) + + const addBounds = ( id: string, bounds: Required ) => { + const { minX, maxX, minY, maxY } = bounds; + addIntervalX( id, [minX, maxX] ) + addIntervalY( id, [minY, maxY] ) + } + + const remBounds = (id: string) => { + remIntervalX(id) + remIntervalY(id) + } + + const bounds = { + minX: boundsX[0], + maxX: boundsX[1], + minY: boundsY[0], + maxY: boundsY[1] + } + + return { bounds, addBounds, remBounds } +} + +export default useMinMaxAxis; \ No newline at end of file diff --git a/frontend/src/hooks/useSize.tsx b/frontend/src/hooks/useSize.tsx index 9674d561..a82c64f8 100644 --- a/frontend/src/hooks/useSize.tsx +++ b/frontend/src/hooks/useSize.tsx @@ -1,25 +1,27 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; -const useSize = (ref: React.MutableRefObject): [number, number] => { - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - useEffect(() => { - if (ref.current === undefined) return; +const useSize = (ref: React.MutableRefObject): [number, number] => { + const [width, setWidth] = useState(0) + const [height, setHeight] = useState(0) - const updateSize = () => { - const { width, height } = (ref.current as any).getBoundingClientRect(); - setWidth(width); - setHeight(height); - }; + useEffect( () => { - updateSize(); + if( ref.current === undefined ) return; - window.addEventListener('resize', updateSize); - return () => window.removeEventListener('resize', updateSize); - }, [ref]); + const updateSize = () => { + const { width, height } = (ref.current as any).getBoundingClientRect() + setWidth(width) + setHeight(height) + } - return [width, height]; -}; + updateSize() -export default useSize; + window.addEventListener('resize', updateSize) + return () => window.removeEventListener('resize', updateSize) + }, [ref]) + + return [width, height] +} + +export default useSize; \ No newline at end of file diff --git a/frontend/src/models/graph.ts b/frontend/src/models/graph.ts index 5517a20b..f4e28da3 100644 --- a/frontend/src/models/graph.ts +++ b/frontend/src/models/graph.ts @@ -1,9 +1,8 @@ -// model and properties for conditions -// used in Condition(ML) page + export interface ConditionType { - name: string; - min: number; - max: number; - grid: boolean; - samples?: number; -} + name: string; + min: number; + max: number; + grid: boolean; + samples?: number; +} \ No newline at end of file diff --git a/frontend/src/models/map.ts b/frontend/src/models/map.ts index 234ae1c0..7b5e72ad 100644 --- a/frontend/src/models/map.ts +++ b/frontend/src/models/map.ts @@ -1,6 +1,9 @@ + + + export interface MapBounds { - minLat: number; - maxLat: number; - minLng: number; - maxLng: number; -} + minLat: number; + maxLat: number; + minLng: number; + maxLng: number; +} \ No newline at end of file diff --git a/frontend/src/pages/RoadConditions.tsx b/frontend/src/pages/RoadConditions.tsx index f62f978c..3048cbdb 100644 --- a/frontend/src/pages/RoadConditions.tsx +++ b/frontend/src/pages/RoadConditions.tsx @@ -12,7 +12,7 @@ import { GraphProvider } from "../context/GraphContext"; import "../css/road_conditions.css"; - +//this is to visualise the Road Conditions (GP) map const RoadConditions = () => { const [palette, setPalette] = useState([]) diff --git a/frontend/src/queries/conditions.ts b/frontend/src/queries/conditions.ts index 78b2414a..ace30e32 100644 --- a/frontend/src/queries/conditions.ts +++ b/frontend/src/queries/conditions.ts @@ -1,32 +1,17 @@ -import { MapBounds } from '../models/map'; -import { Condition, WaysConditions } from '../models/path'; -import { asyncPost, post } from './fetch'; +import { MapBounds } from "../models/map" +import { Condition, WaysConditions } from "../models/path" +import { asyncPost, post } from "./fetch" -export const getWaysConditions = ( - type: string, - zoom: number, - setWays: (data: WaysConditions) => void, -) => { - post('/conditions/ways', { type, zoom }, setWays); -}; -export const getConditions = ( - wayId: string, - type: string, - setConditions: (data: Condition[]) => void, -) => { - post('/conditions/way', { wayId, type }, setConditions); -}; +export const getWaysConditions = ( type: string, zoom: number, setWays: (data: WaysConditions) => void ) => { + post( '/conditions/ways', { type, zoom }, setWays ) +} -export const getBoundedWaysConditions = async ( - bounds: MapBounds, - type: string, - zoom: number, -) => { - console.log(bounds); - return await asyncPost('/conditions/bounded/ways', { - ...bounds, - type, - zoom, - }); -}; +export const getConditions = ( wayId: string, type: string, setConditions: (data: Condition[]) => void ) => { + post( '/conditions/way', { wayId, type }, setConditions ) +} + +export const getBoundedWaysConditions = async ( bounds: MapBounds, type: string, zoom: number ) => { + console.log(bounds); + return await asyncPost( '/conditions/bounded/ways', { ...bounds, type, zoom } ) +} \ No newline at end of file diff --git a/frontend/src/queries/fetch.tsx b/frontend/src/queries/fetch.tsx index d7197d94..853d373a 100644 --- a/frontend/src/queries/fetch.tsx +++ b/frontend/src/queries/fetch.tsx @@ -1,44 +1,39 @@ -import axios, { AxiosResponse } from 'axios'; - -const development = - !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; - -const devURL = process.env.REACT_APP_BACKEND_URL_DEV; -const prodURL = process.env.REACT_APP_BACKEND_URL_PROD; - -const getPath = (p: string) => (development ? devURL : prodURL) + p; - -export async function asyncPost( - path: string, - obj: object, -): Promise> { - return axios.get(getPath(path), { - params: obj, - paramsSerializer: (params) => - Object.keys(params) - .map((key: any) => new URLSearchParams(`${key}=${params[key]}`)) - .join('&'), - }); + +import axios, { AxiosResponse } from 'axios' + +const development = !process.env.NODE_ENV || process.env.NODE_ENV === 'development' + +const devURL = process.env.REACT_APP_BACKEND_URL_DEV +const prodURL = process.env.REACT_APP_BACKEND_URL_PROD + +const getPath = (p: string) => ( development ? devURL : prodURL ) + p + +export async function asyncPost(path: string, obj: object ): Promise> +{ + return axios.get( getPath(path), { + params: obj, + paramsSerializer: params => Object.keys(params) + .map( (key: any) => new URLSearchParams(`${key}=${params[key]}`) ) + .join("&") + } ) } -export function get(path: string, callback: (data: T) => void): void { - fetch(getPath(path)) - .then((res) => res.json()) - .then((data) => callback(data)); +export function get(path: string, callback: (data: T) => void): void +{ + fetch(getPath(path)) + .then(res => res.json()) + .then(data => callback(data)); } -export function post( - path: string, - obj: object, - callback: (data: T) => void, -): void { - asyncPost(path, obj).then((res) => callback(res.data)); +export function post(path: string, obj: object, callback: (data: T) => void): void +{ + asyncPost(path, obj).then(res => callback(res.data)); } -export const put = (path: string, obj: object): void => { - axios.put(getPath(path), obj); -}; +export const put = ( path: string, obj: object ): void => { + axios.put( getPath(path), obj ) +} -export const deleteReq = (path: string): void => { - axios.delete(getPath(path)); -}; +export const deleteReq = ( path: string ): void => { + axios.delete( getPath(path) ) +} \ No newline at end of file