import React, { ChangeEvent, useEffect, useState } from 'react' import { CircleMarker, LayerGroup, LayersControl, MapContainer, Popup, TileLayer, ZoomControl } from 'react-leaflet' import { useHistory } from 'react-router-dom' import chroma from 'chroma-js' import { InputWithLabel } from '../../components/inputWithLabel/InputWithLabel' import DatePicker from 'react-datepicker' import Input from '../../components/input/Input' import styled from 'styled-components' import { distance } from '../../style/sizes' import Select from '../../components/select/Select' import Button from '../../components/button/Button' import { ClipLoader } from 'react-spinners' import { BikePointProperty } from '../../../../backend/src/entities/BikePoint' interface IBikePoint { id: string commonName: string lat: number lon: number additionalProperties: BikePointProperty[] } interface IBikePointActivity { rentals: number returns: number rentalsReturnsImbalance: number } interface IBikePointsActivity { [bikePointId: string]: IBikePointActivity } interface INumberRange { min: number max: number } interface IBikePointsActivityResponse { bikePointsActivity: IBikePointsActivity rentalsRange: INumberRange returnsRange: INumberRange rentalsReturnsImbalanceRange: INumberRange } interface IColorRange { min: string max: string } type ActivityType = 'rentals' | 'returns' | 'rentalsReturnsImbalance' /** * This page contains a map of London. The user can select from different interactive visualizations. */ export const BikeSharingMap = () => { const history = useHistory() const [startTimeStamp, setStartTimestamp] = useState(Date.UTC(2015, 0, 4, 0, 0)) const [endTimeStamp, setEndTimeStamp] = useState(Date.UTC(2015, 0, 19, 0, 0)) const [activityType, setActivityType] = useState('rentals') const [colorRange, setColorRange] = useState({min: '#fcf2ae', max: '#a90e00'}) const [bikePoints, setBikePoints] = useState(undefined) const [data, setData] = useState(undefined) const [loadingState, setLoadingState] = useState(false) // first, fetch bike-points with their coordinates to show on map fast useEffect(() => { (async () => { const response = await fetch(`http://localhost:8081/api/bike-points/all`) const jsonResponse = await response.json() setBikePoints(jsonResponse.bikePoints) })() }, []) const fetchData = async () => { setLoadingState(true) const response = await fetch(`http://localhost:8081/api/bike-points-activity?from=${startTimeStamp / 1000}&to=${endTimeStamp / 1000}`) const jsonResponse = await response.json() setData(jsonResponse) setLoadingState(false) } // fetch data for bike stations from our server once at beginning, later only on button click useEffect(() => {fetchData()}, []) return ( <>

Map view

Visualize the number of rented or returned bikes at bike points in the chosen timeframe. Or the imbalance between the two.

An intense color indicates a high number, relative to the other ones. The color range always adjusts to the current max and min value.

Black dots represent bike points, for which no data is available in the selected timeframe.

setStartTimestamp(date.getTime())} dateFormat='yyyy/MM/dd, HH:mm' showTimeSelect timeFormat='HH:mm' timeIntervals={5} customInput={} /> setEndTimeStamp(date.getTime())} dateFormat='yyyy/MM/dd, HH:mm' showTimeSelect timeFormat='HH:mm' timeIntervals={5} customInput={} />    {loadingState && }
{data ? getValuesRange(activityType, data).min : '-'}
{data ? getValuesRange(activityType, data).max : '-'}
{ bikePoints?.map(bikePoint => { const activities = data?.bikePointsActivity[bikePoint.id] const numberToVisualize = activities && activities[activityType] const color = numberToVisualize && data ? getColor(numberToVisualize, activityType, data, colorRange) : 'black' return (

{bikePoint.commonName}


Number of docks: {Number(bikePoint.additionalProperties.find(additionalProperty => additionalProperty.key==='NbDocks')?.value ?? 'unknown')}

Rentals: {activities?.rentals ?? '-'}
Returns: {activities?.returns ?? '-'}
Returns/Rentals imbalance: {activities?.rentalsReturnsImbalance ?? '-'}

) }) }
) } // returns interpolated color according to provided value, using current selected range const getColor = (value: number, activityType: ActivityType, data: IBikePointsActivityResponse, colorRange: IColorRange) => { const valuesRange = getValuesRange(activityType, data) return chroma.scale([colorRange.min, colorRange.max]).domain([valuesRange.min, valuesRange.max])(value).hex() } // returns value range for selected data const getValuesRange = (activityType: ActivityType, data: IBikePointsActivityResponse) => { let rangeValues: INumberRange switch (activityType) { case 'rentals': rangeValues = data.rentalsRange break case 'returns': rangeValues = data.returnsRange break case 'rentalsReturnsImbalance': rangeValues = data.rentalsReturnsImbalanceRange break } return rangeValues } const StyledGradient = styled.div` height: 2rem; width: 100%; background: linear-gradient(to right, ${props => props.min} 0%, ${props => props.max} 100%); ` const StyledLegendText = styled.div` width: 100%; display: flex; flex-direction: row; justify-content: space-between; ` const StyledControl = styled.div` padding: ${distance.large}; position: absolute; height: 100vh; max-width: 30vw; min-width: 24vw; z-index: 2; background: ${({theme}) => theme.colors.backgroundSecondary}; display: flex; flex-direction: column; justify-content: space-between; ` const Black = styled.div` color: black; ` const StyledRow = styled.div` padding: ${distance.verySmall}; display: flex; flex-direction: row; `