115 |
116 | { (this.props.refSpeedComparisonEnabled) ? 'Percent change in speed by hour-of-day' : (this.props.refSpeedEnabled) ? 'Reference speed (KPH) by hour-of-day' : 'Average speed (KPH) by hour-of-day' }
117 |
118 |
{ this.hourlyChartEl = ref }} />
119 |
120 |
121 |
122 |
123 | {
128 | this.props.dispatch(setRefSpeedComparisonEnabled(data.checked))
129 | }}
130 | />
131 |
132 |
133 |
134 |
135 |
136 | {
141 | this.props.dispatch(setRefSpeedEnabled(data.checked))
142 | }}
143 | />
144 |
145 |
146 |
147 | )
148 | }
149 | }
150 |
151 | function mapStateToProps (state) {
152 | return {
153 | dayFilter: state.date.dayFilter,
154 | hourFilter: state.date.hourFilter,
155 | speedsBinnedByHour: state.barchart.speedsBinnedByHour,
156 | refSpeedsBinnedByHour: state.barchart.refSpeedsBinnedByHour,
157 | percentDiffsBinnedByHour: state.barchart.percentDiffsBinnedByHour,
158 | refSpeedComparisonEnabled: state.app.refSpeedComparisonEnabled,
159 | refSpeedEnabled: state.app.refSpeedEnabled
160 | }
161 | }
162 |
163 | export default connect(mapStateToProps)(TimeFilters)
164 |
--------------------------------------------------------------------------------
/src/components/MapSearchBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Autosuggest from 'react-autosuggest'
3 | import PropTypes from 'prop-types'
4 | import { Button, Icon } from 'semantic-ui-react'
5 | import { throttle } from 'lodash'
6 | import { parseQueryString } from '../../lib/url-state'
7 | import './MapSearchBar.css'
8 |
9 | class MapSearchBar extends React.Component {
10 | static propTypes = {
11 | setLocation: PropTypes.func.isRequired,
12 | clearLabel: PropTypes.func.isRequired,
13 | recenterMap: PropTypes.func.isRequired,
14 | apiKey: PropTypes.string.isRequired,
15 | map: PropTypes.object
16 | }
17 |
18 | constructor (props) {
19 | super(props)
20 |
21 | const newValue = parseQueryString('label')
22 | this.state = {
23 | value: newValue || '',
24 | placeholder: 'Search for an address or a place',
25 | suggestions: []
26 | }
27 |
28 | this.throttleMakeRequest = throttle(this.makeRequest, 250)
29 | }
30 |
31 | componentDidMount () {
32 | // If link has label query, display search bar and expand search icon to fit search bar
33 | if (this.state.value !== '') {
34 | this.refs.searchBar.classList.add('search-bar__expanded')
35 | this.refs.searchButton.ref.classList.add('search-bar__expanded')
36 | }
37 | }
38 |
39 | // Will be called every time you need to recalculate suggestions
40 | onSuggestionsFetchRequested = ({value}) => {
41 | if (value.length >= 2) {
42 | this.autocomplete(value)
43 | }
44 | }
45 |
46 | // Will be called every time you need to set suggestions to []
47 | onSuggestionsClearRequested = () => {
48 | this.setState({
49 | suggestions: []
50 | })
51 | }
52 |
53 | // Teach Autosuggest what should be input value when suggestion is clicked
54 | getSuggestionValue = (suggestion) => {
55 | return suggestion.formatted
56 | }
57 |
58 | // Will be called every time suggestion is selected via mouse or keyboard
59 | onSuggestionSelected = (event, {suggestion, suggestionValue, suggestionIndex, sectionIndex, method}) => {
60 | event.preventDefault()
61 | const lat = suggestion.geometry.lat
62 | const lng = suggestion.geometry.lng
63 | const latlng = [lat, lng]
64 |
65 | // Stores latlng and name of selected location in Redux
66 | this.props.setLocation(latlng, suggestionValue)
67 | // Recenters map to the selected location's latlng
68 | const zoom = this.props.map.zoom
69 | // If user is below zoom 10, set to 10
70 | if (zoom < 10) {
71 | this.props.recenterMap(latlng, 10)
72 | } else {
73 | this.props.recenterMap(latlng, zoom)
74 | }
75 | }
76 |
77 | renderSuggestion = (suggestion, {query, isHighlighted}) => {
78 | const label = suggestion.formatted
79 |
80 | // Highlight the input query
81 | const r = new RegExp(`(${query})`, 'gi')
82 | const highlighted = label.split(r)
83 | for (let i = 0; i < highlighted.length; i++) {
84 | if (highlighted[i].toLowerCase() === query.toLowerCase()) {
85 | highlighted[i] =
{highlighted[i]}
86 | }
87 | }
88 |
89 | return (
90 |
91 | {highlighted}
92 |
93 | )
94 | }
95 |
96 | onChangeAutosuggest = (event, {newValue, method}) => {
97 | this.setState({
98 | value: newValue
99 | })
100 | }
101 |
102 | // Makes autocomplete request to Mapzen Search based on what user has typed
103 | autocomplete = (query) => {
104 | // const endpoint = `https://search.mapzen.com/v1/autocomplete?text=${query}&api_key=${this.props.apiKey}`
105 | // this.throttleMakeRequest(endpoint)
106 | }
107 |
108 | // Makes search request based on what user has entered
109 | search = (query) => {
110 | const endpoint = `https://api.opencagedata.com/geocode/v1/json?q=${query}&key=${this.props.apiKey}`
111 | this.throttleMakeRequest(endpoint)
112 | }
113 |
114 | makeRequest = (endpoint) => {
115 | window.fetch(endpoint)
116 | .then(response => response.json())
117 | .then((results) => {
118 | this.setState({
119 | suggestions: results.results
120 | })
121 | })
122 | }
123 |
124 | // Clear button only appears when there's more than two characters in input
125 | renderClearButton = (value) => {
126 | if (value.length > 2) {
127 | return (
128 |
129 | )
130 | }
131 | }
132 |
133 | clearSearch = (event) => {
134 | // Set state value back to empty string
135 | this.setState({
136 | value: ''
137 | })
138 | // Clears suggestions
139 | this.onSuggestionsClearRequested()
140 | this.props.clearLabel()
141 | }
142 |
143 | // Now Autosuggest component is wrapped in a form so that when 'enter' is pressed, suggestions container is not closed automatically
144 | // Instead search results are returned in suggestions container
145 | handleSubmit = (event) => {
146 | event.preventDefault()
147 | const inputValue = this.autosuggestBar.input.value
148 | if (inputValue !== '') {
149 | this.search(inputValue)
150 | }
151 | }
152 |
153 | // When search button is clicked, autosuggest bar gets displayed
154 | handleClick = (event) => {
155 | const searchButton = this.refs.searchButton.ref
156 | const inputContainer = this.refs.searchBar
157 | // Display search bar and expand search icon size when clicked on
158 | inputContainer.classList.toggle('search-bar__expanded')
159 | searchButton.classList.toggle('search-bar__expanded')
160 | this.autosuggestBar.input.focus()
161 | }
162 |
163 | render () {
164 | const inputProps = {
165 | placeholder: this.state.placeholder,
166 | value: this.state.value,
167 | onChange: this.onChangeAutosuggest
168 | }
169 | const inputVal = this.state.value
170 |
171 | return (
172 |
188 | )
189 | }
190 | }
191 |
192 | export default MapSearchBar
193 |
--------------------------------------------------------------------------------
/src/app/region-bounds.js:
--------------------------------------------------------------------------------
1 | /* global map, L */
2 | import 'leaflet-shades'
3 | import store from '../store'
4 | import { setBounds } from '../store/actions/view'
5 | import { getBboxArea } from './region'
6 | import { getDateRange } from './dataGeojson'
7 |
8 | const PAN_MAP_RATIO = 0.75
9 |
10 | // Store for existing bounds.
11 | const bounds = []
12 | let handlersAdded = false
13 | let shades
14 |
15 | // Subscribe to changes in state to affect the behavior of Leaflet.Editable.
16 | store.subscribe(() => {
17 | const state = store.getState()
18 | // If bounds are cleared from state, remove current bounds.
19 | if (!state.view.bounds) removeAllExistingBounds()
20 |
21 | // While data is still being rendered, disable interactivity of bounds
22 | if (state.loading.isLoading && bounds.length) {
23 | bounds.forEach(function (bound) {
24 | setBoundToDisabledAppearance(bound)
25 | })
26 | } else if (!state.loading.isLoading && bounds.length) {
27 | // If data is not being loaded, check if bounds is bigger than map container
28 | // If so, disable interactivity of bounds, else reenable them
29 | bounds.forEach(function (bound) {
30 | if (!compareRegionAndMap(bound)) {
31 | removeDisabledAppearance(bound)
32 | bound.editor.enable()
33 | }
34 | })
35 | }
36 |
37 | // If select mode has changed, stop any existing drawing interaction.
38 | if (state.app.analysisMode !== 'REGION' && typeof map !== 'undefined' && map.editTools) {
39 | map.editTools.stopDrawing()
40 | if (shades) map.removeLayer(shades)
41 | }
42 | })
43 |
44 | /**
45 | * Compares selected region's area to map container area
46 | * Returns true if selected region's area is bigger than map container area by
47 | * a certain percentage labeled PAN_OUT_VALUE
48 | *
49 | * @param {LatLngBounds} bounds - current bounds of selected region
50 | */
51 | function compareRegionAndMap (bounds) {
52 | const regionBounds = bounds.getBounds()
53 | const northEastPoint = map.latLngToContainerPoint(regionBounds.getNorthEast())
54 | const southWestPoint = map.latLngToContainerPoint(regionBounds.getSouthWest())
55 | const bbox = {
56 | north: northEastPoint.x,
57 | east: northEastPoint.y,
58 | south: southWestPoint.x,
59 | west: southWestPoint.y
60 | }
61 | const regionArea = getBboxArea(bbox)
62 | const mapSize = map.getSize()
63 | const mapArea = mapSize.x * mapSize.y
64 | const ratio = regionArea / mapArea
65 | return ratio > PAN_MAP_RATIO
66 | }
67 |
68 | /**
69 | * Removes an existing bounds.
70 | *
71 | * @param {Number} index - remove the bounds at this index in the cache.
72 | * Defaults to the earliest bounds (at index 0).
73 | */
74 | function removeExistingBounds (index = 0) {
75 | if (bounds[index] && bounds[index].remove) {
76 | // Manual cleanup on Leaflet
77 | bounds[index].remove()
78 |
79 | // Remove from memory
80 | bounds.splice(index, 1)
81 | }
82 | }
83 |
84 | function removeAllExistingBounds () {
85 | while (bounds.length) {
86 | bounds[0].remove()
87 | bounds.shift()
88 | }
89 | }
90 |
91 | /**
92 | * Re-enables the interactivity of a boundary and
93 | * removes the appearance of a disabled state.
94 | *
95 | * @param {LatLngBounds} bound - boundary object to change.
96 | */
97 | function removeDisabledAppearance (bound) {
98 | bound.setStyle({
99 | weight: 3,
100 | color: '#3388ff',
101 | fill: 'transparent',
102 | dashArray: null
103 | })
104 | bound._path.classList.remove('map-bounding-box-disabled')
105 | }
106 |
107 | /**
108 | * Sets the appearance and interactivity of a boundary to be in disabled state.
109 | *
110 | * @param {LatLngBounds} bound - boundary object to change.
111 | */
112 | function setBoundToDisabledAppearance (bound) {
113 | bound.setStyle({
114 | weight: 1,
115 | color: '#aaa',
116 | fill: '#aaa',
117 | fillOpacity: 0,
118 | dashArray: [5, 3]
119 | })
120 | bound._path.classList.add('map-bounding-box-disabled')
121 | bound.editor.disable()
122 | }
123 |
124 | function storeBounds (bounds) {
125 | const precision = 6
126 | const north = bounds.getNorth().toFixed(precision)
127 | const south = bounds.getSouth().toFixed(precision)
128 | const east = bounds.getEast().toFixed(precision)
129 | const west = bounds.getWest().toFixed(precision)
130 |
131 | // Store it.
132 | store.dispatch(setBounds({ north, south, east, west }))
133 | }
134 |
135 | function onDrawingFinished (event) {
136 | const region = {
137 | northEast: event.layer.getBounds().getNorthEast(),
138 | southWest: event.layer.getBounds().getSouthWest()
139 | }
140 | getDateRange(region.northEast, region.southWest)
141 | // The newly created rectangle is stored at `event.layer`
142 | bounds.push(event.layer)
143 |
144 | // Remove previous bounds after the new one has been drawn.
145 | if (bounds.length > 1) {
146 | removeExistingBounds(0)
147 | }
148 | }
149 |
150 | function onDrawingEdited (event) {
151 | storeBounds(event.layer.getBounds())
152 | const bounds = {
153 | northEast: event.layer.getBounds().getNorthEast(),
154 | southWest: event.layer.getBounds().getSouthWest()
155 | }
156 | getDateRange(bounds.northEast, bounds.southWest)
157 | }
158 |
159 | function onMapMoved (event) {
160 | if (!bounds[0]) return
161 | if (compareRegionAndMap(bounds[0])) {
162 | setBoundToDisabledAppearance(bounds[0])
163 | } else {
164 | removeDisabledAppearance(bounds[0])
165 | bounds[0].editor.enable()
166 | }
167 | }
168 |
169 | function addEventListeners () {
170 | map.on('editable:drawing:commit', onDrawingFinished)
171 | map.on('editable:vertex:dragend', onDrawingEdited)
172 | map.on('editable:dragend', onDrawingEdited)
173 | map.on('moveend', onMapMoved)
174 | }
175 |
176 | /**
177 | * Function for drawing new viewport bounds.
178 | *
179 | * @param {Object} event - from onClick handler
180 | * @param {Function} callback - optional. Callback function to call after the
181 | * bounds has finished drawing.
182 | */
183 | export function startDrawingBounds () {
184 | if (!handlersAdded) {
185 | addEventListeners()
186 | handlersAdded = true
187 | }
188 |
189 | // Remove the handles on existing bounds, but don't remove yet. It remains
190 | // as a "ghost" so that it can be referenced when new bounds are drawn over it.
191 | if (bounds.length) {
192 | bounds.forEach(setBoundToDisabledAppearance)
193 | }
194 |
195 | map.editTools.startRectangle()
196 | shades = new L.LeafletShades()
197 | shades.addTo(map)
198 | }
199 |
200 | export function drawBounds ({ west, south, east, north }) {
201 | const rect = L.rectangle([
202 | [north, west],
203 | [south, east]
204 | ]).addTo(map)
205 | rect.enableEdit()
206 | shades = new L.LeafletShades({bounds: rect.getBounds()})
207 | shades.addTo(map)
208 |
209 | if (!handlersAdded) {
210 | addEventListeners()
211 | handlersAdded = true
212 | }
213 | bounds.push(rect)
214 | storeBounds(rect.getBounds())
215 | compareRegionAndMap(rect)
216 | }
217 |
--------------------------------------------------------------------------------
/src/lib/__tests__/tiles.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import {
3 | getTilesForBbox,
4 | getTilesForBufferedBbox,
5 | getTileUrlSuffix,
6 | parseSegmentId
7 | } from '../tiles'
8 |
9 | it('returns a set of Valhalla tiles given a bounding box', () => {
10 | // Switzerland
11 | const result = getTilesForBbox(7.946610, 46.489815, 9.996990, 47.589906)
12 | const expected = [[2, 785551], [2, 786991], [2, 788431], [2, 789871], [2, 791311], [2, 792751], [2, 785552], [2, 786992], [2, 788432], [2, 789872], [2, 791312], [2, 792752], [2, 785553], [2, 786993], [2, 788433], [2, 789873], [2, 791313], [2, 792753], [2, 785554], [2, 786994], [2, 788434], [2, 789874], [2, 791314], [2, 792754], [2, 785555], [2, 786995], [2, 788435], [2, 789875], [2, 791315], [2, 792755], [2, 785556], [2, 786996], [2, 788436], [2, 789876], [2, 791316], [2, 792756], [2, 785557], [2, 786997], [2, 788437], [2, 789877], [2, 791317], [2, 792757], [2, 785558], [2, 786998], [2, 788438], [2, 789878], [2, 791318], [2, 792758], [2, 785559], [2, 786999], [2, 788439], [2, 789879], [2, 791319], [2, 792759], [1, 49147], [1, 49507], [1, 49148], [1, 49508], [1, 49149], [1, 49509], [0, 3106], [0, 3107]]
13 |
14 | expect(result).toEqual(expected)
15 | })
16 |
17 | it('returns a set of Valhalla tiles given a bounding box that crosses the antimeridian', () => {
18 | // Fiji
19 | const result = getTilesForBbox(177.345441, -18.185201, -179.723954, -16.116797)
20 | const expected = [[2, 414709], [2, 416149], [2, 417589], [2, 419029], [2, 420469], [2, 421909], [2, 423349], [2, 424789], [2, 426229], [2, 414710], [2, 416150], [2, 417590], [2, 419030], [2, 420470], [2, 421910], [2, 423350], [2, 424790], [2, 426230], [2, 414711], [2, 416151], [2, 417591], [2, 419031], [2, 420471], [2, 421911], [2, 423351], [2, 424791], [2, 426231], [2, 414712], [2, 416152], [2, 417592], [2, 419032], [2, 420472], [2, 421912], [2, 423352], [2, 424792], [2, 426232], [2, 414713], [2, 416153], [2, 417593], [2, 419033], [2, 420473], [2, 421913], [2, 423353], [2, 424793], [2, 426233], [2, 414714], [2, 416154], [2, 417594], [2, 419034], [2, 420474], [2, 421914], [2, 423354], [2, 424794], [2, 426234], [2, 414715], [2, 416155], [2, 417595], [2, 419035], [2, 420475], [2, 421915], [2, 423355], [2, 424795], [2, 426235], [2, 414716], [2, 416156], [2, 417596], [2, 419036], [2, 420476], [2, 421916], [2, 423356], [2, 424796], [2, 426236], [2, 414717], [2, 416157], [2, 417597], [2, 419037], [2, 420477], [2, 421917], [2, 423357], [2, 424797], [2, 426237], [2, 414718], [2, 416158], [2, 417598], [2, 419038], [2, 420478], [2, 421918], [2, 423358], [2, 424798], [2, 426238], [2, 414719], [2, 416159], [2, 417599], [2, 419039], [2, 420479], [2, 421919], [2, 423359], [2, 424799], [2, 426239], [2, 414720], [2, 416160], [2, 417600], [2, 419040], [2, 420480], [2, 421920], [2, 423360], [2, 424800], [2, 426240], [1, 25917], [1, 26277], [1, 26637], [1, 25918], [1, 26278], [1, 26638], [1, 25919], [1, 26279], [1, 26639], [1, 25920], [1, 26280], [1, 26640], [0, 1619], [0, 1709], [0, 1620], [0, 1710], [2, 413280], [2, 414720], [2, 416160], [2, 417600], [2, 419040], [2, 420480], [2, 421920], [2, 423360], [2, 424800], [2, 413281], [2, 414721], [2, 416161], [2, 417601], [2, 419041], [2, 420481], [2, 421921], [2, 423361], [2, 424801], [1, 25560], [1, 25920], [1, 26280], [0, 1530], [0, 1620]]
21 |
22 | expect(result).toEqual(expected)
23 | })
24 |
25 | it('returns a set of Valhalla tiles given a bounding box, with a buffer', () => {
26 | // Switzerland
27 | const result = getTilesForBufferedBbox(7.946610, 46.489815, 9.996990, 47.589906)
28 | const expected = [[2, 785551], [2, 786991], [2, 788431], [2, 789871], [2, 791311], [2, 792751], [2, 785552], [2, 786992], [2, 788432], [2, 789872], [2, 791312], [2, 792752], [2, 785553], [2, 786993], [2, 788433], [2, 789873], [2, 791313], [2, 792753], [2, 785554], [2, 786994], [2, 788434], [2, 789874], [2, 791314], [2, 792754], [2, 785555], [2, 786995], [2, 788435], [2, 789875], [2, 791315], [2, 792755], [2, 785556], [2, 786996], [2, 788436], [2, 789876], [2, 791316], [2, 792756], [2, 785557], [2, 786997], [2, 788437], [2, 789877], [2, 791317], [2, 792757], [2, 785558], [2, 786998], [2, 788438], [2, 789878], [2, 791318], [2, 792758], [2, 785559], [2, 786999], [2, 788439], [2, 789879], [2, 791319], [2, 792759], [2, 785560], [2, 787000], [2, 788440], [2, 789880], [2, 791320], [2, 792760], [1, 49147], [1, 49507], [1, 49148], [1, 49508], [1, 49149], [1, 49509], [1, 49150], [1, 49510], [0, 3106], [0, 3107]]
29 |
30 | expect(result).toEqual(expected)
31 | })
32 |
33 | describe('getTileUrlSuffix', () => {
34 | it('creates a directory/file path from a tile level and id', () => {
35 | const result1 = getTileUrlSuffix(2, 1036752)
36 | const result2 = getTileUrlSuffix(2, 42)
37 | const result3 = getTileUrlSuffix(1, 54)
38 | const result4 = getTileUrlSuffix(1, 64001)
39 | const result5 = getTileUrlSuffix(0, 79)
40 | const result6 = getTileUrlSuffix(0, 4001)
41 |
42 | expect(result1).toEqual('2/001/036/752')
43 | expect(result2).toEqual('2/000/000/042')
44 | expect(result3).toEqual('1/000/054')
45 | expect(result4).toEqual('1/064/001')
46 | expect(result5).toEqual('0/000/079')
47 | expect(result6).toEqual('0/004/001')
48 | })
49 |
50 | it('creates a directory/file path from a tile level and id tuple', () => {
51 | const result1 = getTileUrlSuffix([2, 1036752])
52 | const result2 = getTileUrlSuffix([2, 42])
53 | const result3 = getTileUrlSuffix([1, 54])
54 | const result4 = getTileUrlSuffix([1, 64001])
55 | const result5 = getTileUrlSuffix([0, 79])
56 | const result6 = getTileUrlSuffix([0, 4001])
57 |
58 | expect(result1).toEqual('2/001/036/752')
59 | expect(result2).toEqual('2/000/000/042')
60 | expect(result3).toEqual('1/000/054')
61 | expect(result4).toEqual('1/064/001')
62 | expect(result5).toEqual('0/000/079')
63 | expect(result6).toEqual('0/004/001')
64 | })
65 |
66 | it('creates a directory/file path from a tile level and id objects', () => {
67 | const result1 = getTileUrlSuffix({ level: 2, tile: 1036752 })
68 | const result2 = getTileUrlSuffix({ level: 2, tile: 42 })
69 | const result3 = getTileUrlSuffix({ level: 1, tile: 54 })
70 | const result4 = getTileUrlSuffix({ level: 1, tile: 64001 })
71 | const result5 = getTileUrlSuffix({ level: 0, tile: 79 })
72 | const result6 = getTileUrlSuffix({ level: 0, tile: 4001 })
73 |
74 | expect(result1).toEqual('2/001/036/752')
75 | expect(result2).toEqual('2/000/000/042')
76 | expect(result3).toEqual('1/000/054')
77 | expect(result4).toEqual('1/064/001')
78 | expect(result5).toEqual('0/000/079')
79 | expect(result6).toEqual('0/004/001')
80 | })
81 |
82 | it('works with Array.map', () => {
83 | const suffixes = [
84 | { level: 2, tile: 1036752 },
85 | { level: 2, tile: 42 },
86 | { level: 1, tile: 54 },
87 | { level: 1, tile: 64001 },
88 | { level: 0, tile: 79 },
89 | { level: 0, tile: 4001 }
90 | ]
91 | const expected = [
92 | '2/001/036/752',
93 | '2/000/000/042',
94 | '1/000/054',
95 | '1/064/001',
96 | '0/000/079',
97 | '0/004/001'
98 | ]
99 |
100 | const results = suffixes.map(getTileUrlSuffix)
101 | expect(results).toEqual(expected)
102 | })
103 | })
104 |
105 | it('parses a segment id from trace_attributes', () => {
106 | const id = 983044211424
107 | const result = parseSegmentId(id)
108 |
109 | expect(result.id).toEqual(id)
110 | expect(result.level).toEqual(0)
111 | expect(result.tileIdx).toEqual(2140)
112 | expect(result.segmentIdx).toEqual(29297)
113 | })
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/src/app/region.js:
--------------------------------------------------------------------------------
1 | import { uniq } from 'lodash'
2 | import { getTilesForBbox, getTileUrlSuffix, parseSegmentId } from '../lib/tiles'
3 | import { merge } from '../lib/geojson'
4 | import { getTangramLayer, setDataSource, getCurrentConfig, setCurrentConfig } from '../lib/tangram'
5 | import { fetchDataTiles } from './data'
6 | import { addSpeedToMapGeometry, prepareDataForBarChart } from './processing'
7 | import store from '../store'
8 | import { setGeoJSON } from '../store/actions/view'
9 | import { startLoading, stopLoading, hideLoading } from '../store/actions/loading'
10 | import { clearBarchart, setBarchartData } from '../store/actions/barchart'
11 | import { setRouteError } from '../store/actions/route'
12 | import { displayRegionInfo } from './route-info'
13 | import mathjs from 'mathjs'
14 |
15 | const LINE_OVERLAP_BUFFER = 0.0003
16 | const MAX_AREA_BBOX = 0.01
17 |
18 | const OSMLRCache = {}
19 |
20 | function getSuffixes (bbox) {
21 | const tiles = getTilesForBbox(bbox.min_lon, bbox.min_lat, bbox.max_lon, bbox.max_lat)
22 | // Filter out tiles with level 2, no data for those
23 | const downloadTiles = tiles.filter(function (tile) { return tile[0] !== 2 })
24 | // Get suffixes of these tiles
25 | const suffixes = downloadTiles.map(i => getTileUrlSuffix(i))
26 | return suffixes
27 | }
28 |
29 | /**
30 | * Goes through each coordinate, each line, each point, and removes (in place)
31 | * latlngs that are outside bounding box
32 | * It then removes array of points if empty, array of lines if empty, array of coordinates if empty,
33 | * and finally, if that feature has no coordinates, it removes the feature from the array of features
34 | *
35 | * @param {array} features - geojson from OSMLR geometry tile
36 | * @param {object} bounds - the latlngs of the bounding box
37 | * @returns {array} - features that are within the bounding box
38 | */
39 |
40 | function withinBbox (features, bounds) {
41 | // We need to check the geometry.coordinates to check if they're within bounds
42 | // If not within bound, remove entire feature from features
43 | const coordinates = features.map(feature => {
44 | return feature.geometry.coordinates
45 | })
46 |
47 | // Coordinates have array of lines
48 | // Lines have array of points [lng, lat]
49 | for (let lineIndex = coordinates.length - 1; lineIndex >= 0; lineIndex--) {
50 | const line = coordinates[lineIndex]
51 | for (let pointsIndex = line.length - 1; pointsIndex >= 0; pointsIndex--) {
52 | const point = line[pointsIndex]
53 | const lat = point[1]
54 | const lng = point[0]
55 | // Checking if latlng is within bounding box
56 | // If not remove from points
57 | if (lng < Number(bounds.west) - LINE_OVERLAP_BUFFER || lng > Number(bounds.east) + LINE_OVERLAP_BUFFER || lat < Number(bounds.south) - LINE_OVERLAP_BUFFER || lat > Number(bounds.north) + LINE_OVERLAP_BUFFER) {
58 | line.splice(pointsIndex, 1)
59 | }
60 | }
61 | // If no lines in coordinates, remove from coordinates
62 | if (line.length === 0) { coordinates.splice(lineIndex, 1) }
63 | }
64 | // If no coordinates, remove entire feature from array of features
65 | for (let i = features.length - 1; i >= 0; i--) {
66 | const feature = features[i]
67 | if (feature.geometry.coordinates.length === 0) { features.splice(i, 1) }
68 | }
69 | return features
70 | }
71 |
72 | export function getBboxArea (bounds) {
73 | const width = Math.abs(bounds.east - bounds.west)
74 | const height = Math.abs(bounds.north - bounds.south)
75 | const area = width * height
76 | return area
77 | }
78 |
79 | export function clearRegion () {
80 | const config = getCurrentConfig()
81 | delete config.sources.routes
82 | setCurrentConfig(config)
83 | const tangramLayer = getTangramLayer()
84 | tangramLayer.setSelectionEvents({hover: null})
85 | }
86 |
87 | export function showRegion (bounds) {
88 | // If bounds are cleared, remove data source from tangram
89 | if (!bounds) {
90 | clearRegion()
91 | return
92 | }
93 |
94 | store.dispatch(startLoading())
95 |
96 | // If area of bounding box exceeds max_area, display error
97 | const area = getBboxArea(bounds)
98 | if (area > MAX_AREA_BBOX) {
99 | const message = 'Please zoom in and reduce the size of your bounding box'
100 | store.dispatch(setRouteError(message))
101 | store.dispatch(hideLoading())
102 | return
103 | }
104 |
105 | const bbox = {
106 | min_lon: Number(bounds.west),
107 | min_lat: Number(bounds.south),
108 | max_lon: Number(bounds.east),
109 | max_lat: Number(bounds.north)
110 | }
111 | const suffixes = getSuffixes(bbox)
112 |
113 | // Fetch the OSMLR Geometry tiles using the suffixes
114 | fetchOSMLRGeometryTiles(suffixes)
115 | .then((results) => {
116 | const copy = JSON.parse(JSON.stringify(results))
117 | const features = copy.features
118 | // Remove from geojson, routes outside bounding box (bounds)
119 | const regionFeatures = withinBbox(features, bounds)
120 | results.features = regionFeatures
121 |
122 | // Get segment IDs to use later
123 | const segmentIds = features.map(key => {
124 | return key.properties.osmlr_id
125 | })
126 | // Removing duplicates of segment IDs
127 | const parsedIds = uniq(segmentIds).map(parseSegmentId)
128 | // Using segmentIds, fetch data tiles
129 | const date = {
130 | year: store.getState().date.year,
131 | week: store.getState().date.week
132 | }
133 | store.dispatch(clearBarchart())
134 | fetchDataTiles(parsedIds, date)
135 | .then((tiles) => {
136 | if (date.year && date.week) {
137 | let totalPercentDiffArray = mathjs.zeros(7, 24)
138 | let totalSpeedArray = mathjs.zeros(7, 24)
139 | let totalRefSpeedArray = mathjs.zeros(7, 24)
140 | let totalCountArray = mathjs.zeros(7, 24)
141 |
142 | parsedIds.forEach((id, index) => {
143 | addSpeedToMapGeometry(tiles, date, id, features[index].properties)
144 | let dataFromThisSegment = prepareDataForBarChart(tiles, date, id)
145 | if (dataFromThisSegment) {
146 | totalPercentDiffArray = mathjs.add(totalPercentDiffArray, dataFromThisSegment.percentDiff)
147 | totalRefSpeedArray = mathjs.add(totalRefSpeedArray, dataFromThisSegment.refSpeeds)
148 | totalSpeedArray = mathjs.add(totalSpeedArray, dataFromThisSegment.speeds)
149 | totalCountArray = mathjs.add(totalCountArray, dataFromThisSegment.counts)
150 | }
151 | })
152 | store.dispatch(setBarchartData(totalSpeedArray, totalPercentDiffArray, totalCountArray, totalRefSpeedArray))
153 | }
154 | setDataSource('routes', { type: 'GeoJSON', data: results })
155 | results.properties = {
156 | bounds: store.getState().view.bounds
157 | }
158 | // more properties will be added during export
159 |
160 | const tangramLayer = getTangramLayer()
161 | tangramLayer.setSelectionEvents({
162 | hover: function (selection) { displayRegionInfo(selection) }
163 | })
164 |
165 | store.dispatch(setGeoJSON(results))
166 | store.dispatch(stopLoading())
167 | })
168 | })
169 | .catch((error) => {
170 | console.log('[fetchDataTiles error]', error)
171 | store.dispatch(hideLoading())
172 | })
173 | }
174 |
175 | /**
176 | * Fetch requested OSMLR geometry tiles and return its result as a
177 | * single GeoJSON file.
178 | *
179 | * @param {Array
} suffixes - an array of tile path suffixes,
180 | * in the form of `x/xxx/xxx`.
181 | * @return {Promise} - a Promise is returned passing the value of all
182 | * OSMLR geometry tiles, merged into a single GeoJSON.
183 | */
184 | function fetchOSMLRGeometryTiles (suffixes) {
185 | const tiles = suffixes.map(suffix => fetchOSMLRtile(suffix))
186 | return Promise.all(tiles).then(merge)
187 | }
188 |
189 | function fetchOSMLRtile (suffix) {
190 | // If tile already exists in cache, return a copy of it
191 | if (OSMLRCache[suffix]) {
192 | const clone = JSON.parse(JSON.stringify(OSMLRCache[suffix]))
193 | return clone
194 | }
195 | // Otherwise make a request for it, cache it and return tile
196 | const path = store.getState().config.osmlrTileUrl
197 | const url = `${path}${suffix}.json`
198 | return window.fetch(url)
199 | .then(results => results.json())
200 | .then(res => cacheOSMLRTiles(res, suffix))
201 | }
202 |
203 | function cacheOSMLRTiles (tile, index) {
204 | Object.assign(OSMLRCache, {[index]: tile})
205 | return tile
206 | }
207 |
--------------------------------------------------------------------------------