diff --git a/package.json b/package.json index da54043..16c9f7b 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,13 @@ }, "dependencies": { "@conveyal/lonlat": "^1.1.1", - "@conveyal/woonerf": "^2.3.0", - "browsochrones": "^0.9.1", + "@conveyal/woonerf": "^3.0.0", "color": "^1.0.3", "date-fns": "^1.28.5", "debug": "^2.6.8", "font-awesome": "^4.6.3", "isomorphic-fetch": "^2.2.1", - "isomorphic-mapzen-search": "^1.2.0", + "jsolines": "^1.0.2", "leaflet": "^0.7.7", "leaflet-transitivelayer": "^0.2.0", "lodash": "^4.17.4", diff --git a/src/actions/browsochrones.js b/src/actions/browsochrones.js deleted file mode 100644 index 235c7cf..0000000 --- a/src/actions/browsochrones.js +++ /dev/null @@ -1,136 +0,0 @@ -// @flow -import lonlat from '@conveyal/lonlat' -import {decrementFetches, incrementFetches} from '@conveyal/woonerf/fetch' -import Browsochrones from 'browsochrones' -import fetch from 'isomorphic-fetch' -import {search as geocode} from 'isomorphic-mapzen-search' - -import {getAsObject as getHash} from '../utils/hash' -import messages from '../utils/messages' - -import { - addActionLogItem, - fetchAllBrowsochrones, - setAccessibilityToEmptyFor, - setAccessibilityToLoadingFor, - setBrowsochronesInstances, - setEnd, - setEndLabel, - setStart, - setStartLabel, - updateMap -} from '../actions' - -import type {Store} from '../types' - -export default function initialize ({ - browsochrones, - geocoder, - map, - timeCutoff -}: Store) { - const {origins} = browsochrones - const qs = getHash() - return [ - incrementFetches(), - setStartLabel(qs.start), // may not exist - setEndLabel(qs.end), // may not exist - ...origins.map( - (origin, index) => - (qs.start - ? setAccessibilityToLoadingFor({index, name: origin.name}) - : setAccessibilityToEmptyFor({index, name: origin.name})) - ), - geocodeQs({geocoder, qs}).then(([start, end]) => { - const actions = [] - if (start) { - actions.push(setStart(start)) - actions.push( - updateMap({centerCoordinates: lonlat.toLeaflet(start.latlng)}) - ) - } - if (end) actions.push(setEnd(end)) - if (qs.zoom) actions.push(updateMap({zoom: parseInt(qs.zoom, 10)})) - actions.push( - fetchGrids(browsochrones) - .then(grids => - loadAllOrigins({grids, origins, gridNames: browsochrones.grids}) - ) - .then(instances => [ - setBrowsochronesInstances(instances), - start && - fetchAllBrowsochrones({ - browsochronesInstances: instances, - endLatlng: end && end.latlng, - latlng: start.latlng, - timeCutoff: timeCutoff.selected, - zoom: map.zoom - }), - addActionLogItem(messages.Strings.ApplicationReady), - decrementFetches() - ]) - ) - return actions - }) - ] -} - -async function geocodeQs ({geocoder, qs}) { - async function geocodeP (p) { - if (qs[p]) { - const results = await geocode({ - apiKey: process.env.MAPZEN_SEARCH_KEY, - boundary: geocoder.boundary, - focusPoint: geocoder.focusLatlng, - text: qs[p] - }) - if (results.features.length > 0) { - return { - label: results.features[0].properties.label, - latlng: lonlat(results.features[0].geometry.coordinates) - } - } - } - } - return [await geocodeP('start'), await geocodeP('end')] -} - -function fetchGrids ({ - grids, - gridsUrl -}: { - grids: string[], - gridsUrl: string -}): Promise { - return Promise.all( - grids.map(name => - fetch(`${gridsUrl}/${name}.grid`).then(r => r.arrayBuffer()) - ) - ) -} - -function loadAllOrigins ({grids, origins, gridNames}) { - return Promise.all(origins.map(origin => load(origin, grids, gridNames))) -} - -async function load (origin, grids, gridNames) { - const bs = new Browsochrones() - bs.name = origin.name - bs.originsUrl = origin.url - bs.grids = gridNames - const fetches = [ - fetch(`${origin.url}/query.json`).then(res => res.json()), - fetch(`${origin.url}/stop_trees.dat`).then(res => res.arrayBuffer()) - ] - const [query, stopTrees] = await Promise.all(fetches) - await bs.setQuery(query) - await bs.setStopTrees(stopTrees) - await bs.setTransitiveNetwork(query.transitiveData) - - const putGrids = grids.map((grid, index) => - bs.putGrid(gridNames[index], grid) - ) - await Promise.all(putGrids) - - return bs -} diff --git a/src/actions/data.js b/src/actions/data.js new file mode 100644 index 0000000..c1cdd6f --- /dev/null +++ b/src/actions/data.js @@ -0,0 +1,183 @@ +// @flow +import lonlat from '@conveyal/lonlat' +import fetch, {fetchMultiple} from '@conveyal/woonerf/fetch' +import Leaflet from 'leaflet' + +import {ACCESSIBILITY_IS_EMPTY, ACCESSIBILITY_IS_LOADING} from '../constants' +import geocode from './geocode' +import {getAsObject as getHash} from '../utils/hash' + +import { + setEnd, + setOrigin, + setStart, + updateMap +} from '../actions' +import {loadGrid} from './grid' + +import type {LonLat} from '../types' + +const setQuery = (query) => ({type: 'set query', payload: query}) + +export function initialize () { + return (dispatch: Dispatch, getState: any) => { + const state = getState() + const qs = getHash() + const origins = state.data.origins + + const currentZoom = qs.zoom ? parseInt(qs.zoom, 10) : 11 + dispatch(updateMap({zoom: currentZoom})) + + origins.map((origin) => { + dispatch( + qs.start + ? setOrigin({name: origin.name, accessibility: ACCESSIBILITY_IS_LOADING}) + : setOrigin({name: origin.name, accessibility: ACCESSIBILITY_IS_EMPTY}) + ) + }) + + state.data.grids.map((grid) => { + dispatch(loadGrid(grid.name, state.data.gridsUrl)) + }) + + if (qs.start) { + dispatch(setStart({label: qs.start})) + dispatch(geocode(qs.start, (feature) => { + const originLonlat = lonlat(feature.center) + dispatch(setStart({ + label: qs.start, + position: originLonlat + })) + + dispatch(loadOrigins(origins, originLonlat, currentZoom)) + })) + } else { + dispatch(loadOrigins(origins)) + if (qs.end) { + dispatch(setEnd({label: qs.end})) + dispatch(geocode(qs.end, (feature) => { + dispatch([ + setEnd({ + label: qs.end, + position: lonlat(feature.center) + }), + updateMap({centerCoordinates: lonlat.toLeaflet(feature.center)}) + ]) + })) + } + } + } +} + +const loadOrigins = (origins, originLonlat?: LonLat, currentZoom?: number) => + origins.map(origin => loadOrigin(origin, originLonlat, currentZoom)) + +const loadOrigin = (origin, originLonlat?: LonLat, currentZoom?: number) => + fetch({ + url: `${origin.url}/query.json`, + next (response) { + const query = response.value + + if (originLonlat && currentZoom) { + return [ + setQuery(query), + fetchDataForOrigin({...origin, query}, originLonlat, currentZoom) + ] + } else { + return [ + setQuery(query), + setOrigin({ + ...origin, + query + }) + ] + } + } + }) + +export const fetchDataForLonLat = (originLonLat: LonLat) => + (dispatch: Dispatch, getState: any) => { + const state = getState() + const currentZoom = state.map.zoom + dispatch(state.data.origins.map(origin => + fetchDataForOrigin(origin, originLonLat, currentZoom))) + } + +const fetchDataForOrigin = (origin, originLonlat, currentZoom) => { + const originPoint = getOriginPoint(originLonlat, currentZoom, origin.query) + const originIndex = originPoint.x + originPoint.y * origin.query.width + return fetchMultiple({ + fetches: [{ + url: `${origin.url}/${originIndex}_times.dat` + }, { + url: `${origin.url}/${originIndex}_paths.dat` + }], + next: ([timesResponse, pathsResponse]) => + setOrigin({ + ...origin, + originPoint, + travelTimeSurface: parseTimes(timesResponse.value), + paths: parsePaths(pathsResponse.value) + }) + }) +} + +function getOriginPoint (originLonlat, currentZoom: number, query) { + const pixel = Leaflet.CRS.EPSG3857.latLngToPoint( + lonlat.toLeaflet(originLonlat), + currentZoom + ) + const scale = Math.pow(2, query.zoom - currentZoom) + + let {x, y} = pixel + x = x * scale - query.west | 0 + y = y * scale - query.north | 0 + + return {x, y} +} + +const TIMES_GRID_TYPE = 'ACCESSGR' +const TIMES_HEADER_LENGTH = 9 + +function parseTimes (ab: ArrayBuffer) { + const data = new Int32Array(ab) + const headerData = new Int8Array(ab) + const header = {} + header.type = String.fromCharCode(...headerData.slice(0, TIMES_GRID_TYPE.length)) + + if (header.type !== TIMES_GRID_TYPE) { + throw new Error(`Retrieved grid header ${header.type} !== ${TIMES_GRID_TYPE}. Please check your data.`) + } + + let offset = 2 + const version = data[offset++] + const zoom = data[offset++] + const west = data[offset++] + const north = data[offset++] + const width = data[offset++] + const height = data[offset++] + const nSamples = data[offset++] + + return { + version, + zoom, + west, + north, + width, + height, + nSamples, + data: data.slice(TIMES_HEADER_LENGTH) + } +} + +const PATHS_GRID_TYPE = 'PATHGRID' +const PATHS_HEADER_LENGTH = 2 + +function parsePaths (ab: ArrayBuffer) { + const data = new Int32Array(ab) + const headerData = new Int8Array(ab) + + return { + data: + } +} diff --git a/src/actions/geocode.js b/src/actions/geocode.js new file mode 100644 index 0000000..b5a4e2a --- /dev/null +++ b/src/actions/geocode.js @@ -0,0 +1,34 @@ +// @flow +import lonlat from '@conveyal/lonlat' +import fetch from '@conveyal/woonerf/fetch' + +import {MAPBOX_GEOCODING_URL} from '../constants' + +import type {LonLat} from '../types' + +/** + * Format URL for fetching with query parameters + */ +function formatURL (text: string, opts) { + opts.access_token = process.env.MAPBOX_ACCESS_TOKEN + const queryParams = Object.keys(opts).map(k => `${k}=${opts[k]}`).join('&') + return `${MAPBOX_GEOCODING_URL}/${text}.json?${queryParams}` +} + +/** + * Create an action that dispatches the given action on success. + */ +export default function geocode (text: string, nextAction: any) { + return function (dispatch: Dispatch, getState: any) { + const state = getState() + const {geocoder} = state + + dispatch(fetch({ + url: formatURL(text, geocoder), + next: (response) => nextAction(JSON.parse(response.value).features[0]) // Content-Type is application/vnd.geo+json so woonerf/fetch parses as text + })) + } +} + +export const reverse = (position: LonLat, nextAction: any) => + geocode(lonlat.toString(position), nextAction) diff --git a/src/actions/grid.js b/src/actions/grid.js new file mode 100644 index 0000000..3cd3a36 --- /dev/null +++ b/src/actions/grid.js @@ -0,0 +1,57 @@ +// @flow +import fetch from '@conveyal/woonerf/fetch' + +import type {Grid} from '../types' + +function createGrid (data: ArrayBuffer): Grid { + const array = new Int32Array(data, 4 * 5) + const header = new Int32Array(data) + + let min = Infinity + let max = -Infinity + + for (let i = 0, prev = 0; i < array.length; i++) { + array[i] = (prev += array[i]) + if (prev < min) min = prev + if (prev > max) max = prev + } + + const width = header[3] + const height = header[4] + const contains = (x, y) => x >= 0 && x < width && y >= 0 && y < height + + // parse header + return { + zoom: header[0], + west: header[1], + north: header[2], + width, + height, + data: array, + min, + max, + contains, + valueAtPoint (x, y) { + if (contains(x, y)) { + return array[y * width + x] + } else { + return 0 + } + } + } +} + +export function loadGrid (gridName: string, url: string) { + return fetch({ + url: `${url}/${gridName}.grid`, + next (response) { + return { + type: 'set grid', + payload: { + name: gridName, + ...createGrid(response.value) + } + } + } + }) +} diff --git a/src/actions/index.js b/src/actions/index.js index 332b2c2..8e6ef86 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,56 +1,43 @@ // @flow -import lonlat from '@conveyal/lonlat' -import type Browsochrones from 'browsochrones' -import fetch, { - incrementFetches as incrementWork, - decrementFetches as decrementWork -} from '@conveyal/woonerf/fetch' -import {reverse} from 'isomorphic-mapzen-search' -import Leaflet from 'leaflet' import {createAction} from 'redux-actions' import {setKeyTo} from '../utils/hash' -import type {LatLng} from '../types' - const END = 'end' const START = 'start' -const reverseGeocode = ({latlng}) => - reverse({ - apiKey: process.env.MAPZEN_SEARCH_KEY, - point: latlng - }) - -export const addActionLogItem = createAction('add action log item', item => { +export const setOrigin = (payload: any) => ({type: 'set origin', payload}) +export const addActionLogItem = (item: string) => { const payload = typeof item === 'string' ? {text: item} : item return { - createdAt: new Date(), - level: 'info', - ...payload + type: 'add action log item', + payload: { + createdAt: new Date(), + level: 'info', + ...payload + } } -}) +} -export const setEnd = createAction('set end', end => { +export const setActiveOrigin = (name: string) => + ({type: 'set active origin', payload: name}) + +export const setEnd = (end: any) => { setKeyTo(END, end ? end.label : null) - return end -}) + return { + type: 'set end', + payload: end + } +} -export const setStart = createAction('set start', start => { +export const setStart = (start: any) => { setKeyTo(START, start ? start.label : null) - return start -}) - -export const setEndLabel = createAction('set end label', label => { - setKeyTo(END, label) - return label -}) - -export const setStartLabel = createAction('set start label', label => { - setKeyTo(START, label) - return label -}) + return { + type: 'set start', + payload: start + } +} export const clearEnd = createAction('clear end', () => setKeyTo(END, null)) export const clearStart = createAction('clear start', () => @@ -65,13 +52,6 @@ export const setAccessibilityToLoadingFor = createAction( 'set accessibility to loading for' ) -export const setActiveBrowsochronesInstance = createAction( - 'set active browsochrones instance' -) -export const setBrowsochronesInstances = createAction( - 'set browsochrones instances' -) - export const setSelectedTimeCutoff = createAction('set selected time cutoff') export const setDestinationDataFor = createAction('set destination data for') @@ -83,320 +63,3 @@ export const setIsochrone = createAction('set isochrone') export const setIsochroneFor = createAction('set isochrone for') export const updateMap = createAction('update map') - -/** - * What happens on start update: - * - Map marker should get set to the new start immmediately (if it wasn't a drag/drop) - * - If there's no label, the latlng point should be reverse geocoded and saved - * - If Browsochones is loaded, new start data is retreived - * - A new surface is generated - * - A new jsonline generated - * - Accessibility is calculated for grids - */ -export function updateStart ({ - browsochronesInstances, - endLatlng, - latlng, - label, - timeCutoff, - zoom -}: { - browsochronesInstances: Browsochrones[], - endLatlng: LatLng, - latlng: LatLng, - label: string, - timeCutoff: number, - zoom: number -}) { - const actions = [ - addActionLogItem('Generating origins...'), - clearIsochrone(), - ...browsochronesInstances.map((instance, index) => - setAccessibilityToLoadingFor({index, name: instance.name}) - ) - ] - - // TODO: Remove this! - if (label && label.toLowerCase().indexOf('airport') !== -1) { - latlng = { - lat: 39.7146, - lng: -86.2983 - } - } - - if (label) { - actions.push( - addActionLogItem(`Set start address to: ${label}`), - setStart({label, latlng}) - ) - } else { - actions.push( - setStart({latlng}), - addActionLogItem( - `Finding start address for ${lonlat(latlng).toString()}` - ), - reverseGeocode({latlng}).then(({features}) => { - if (!features || features.length < 1) return - const label = features[0].properties.label - return [ - addActionLogItem(`Set start address to: ${label}`), - setStartLabel(label) - ] - }) - ) - } - - if (!browsochronesInstances || browsochronesInstances.length === 0) { - return actions - } - - actions.push( - fetchAllBrowsochrones({ - browsochronesInstances, - endLatlng, - latlng, - timeCutoff, - zoom - }) - ) - - return actions -} - -export function fetchAllBrowsochrones ({ - browsochronesInstances, - endLatlng, - latlng, - timeCutoff, - zoom -}: { - browsochronesInstances: Browsochrones[], - endLatlng?: LatLng, - latlng: LatLng, - timeCutoff: number, - zoom: number -}) { - const point = Leaflet.CRS.EPSG3857.latLngToPoint( - lonlat.toLeaflet(latlng), - zoom - ) - const originPoint = browsochronesInstances[0].pixelToOriginPoint(point, zoom) - if (browsochronesInstances[0].pointInQueryBounds(originPoint)) { - return browsochronesInstances.map((instance, index) => - fetchBrowsochronesFor({ - browsochrones: instance, - endLatlng, - index, - latlng, - timeCutoff, - zoom - }) - ) - } else { - console.log('point out of bounds') // TODO: Handle - } -} - -function fetchBrowsochronesFor ({ - browsochrones, - endLatlng, - index, - latlng, - timeCutoff, - zoom -}) { - const point = browsochrones.pixelToOriginPoint( - Leaflet.CRS.EPSG3857.latLngToPoint(lonlat.toLeaflet(latlng), zoom), - zoom - ) - return [ - incrementWork(), // to include the time taking to set the origin and generate the surface - addActionLogItem(`Fetching origin data for scenario ${index}`), - fetch({ - url: `${browsochrones.originsUrl}/${point.x | 0}/${point.y | 0}.dat`, - next: async (error, response) => { - if (error) { - console.error(error) - } else { - await browsochrones.setOrigin(response.value, point) - await browsochrones.generateSurface() - - return [ - decrementWork(), - generateAccessiblityFor({browsochrones, index, latlng, timeCutoff}), - generateIsochroneFor({browsochrones, index, latlng, timeCutoff}), - endLatlng && - generateDestinationDataFor({ - browsochrones, - startLatlng: latlng, - index, - endLatlng: endLatlng, - zoom - }) - ] - } - } - }) - ] -} - -const storedAccessibility = {} -const storedIsochrones = {} - -function generateAccessiblityFor ({browsochrones, index, latlng, timeCutoff}) { - return [ - incrementWork(), - addActionLogItem(`Generating accessibility surface for scenario ${index}`), - (async () => { - const accessibility = {} - for (const grid of browsochrones.grids) { - const key = `${index}-${lonlat.toString(latlng)}-${timeCutoff}-${grid}` - accessibility[grid] = - storedAccessibility[key] || - (await browsochrones.getAccessibilityForGrid(grid, timeCutoff)) - storedAccessibility[key] = accessibility[grid] - } - return [ - setAccessibilityFor({accessibility, index, name: browsochrones.name}), - decrementWork() - ] - })() - ] -} - -function generateIsochroneFor ({browsochrones, index, latlng, timeCutoff}) { - return [ - incrementWork(), - addActionLogItem(`Generating travel time isochrone for scenario ${index}`), - (async () => { - const key = `${index}-${lonlat.toString(latlng)}-${timeCutoff}` - const isochrone = - storedIsochrones[key] || (await browsochrones.getIsochrone(timeCutoff)) - isochrone.key = key - storedIsochrones[key] = isochrone - - return [setIsochroneFor({isochrone, index}), decrementWork()] - })() - ] -} - -function generateDestinationDataFor ({ - browsochrones, - startLatlng, - index, - endLatlng, - zoom -}) { - return [ - incrementWork(), - addActionLogItem(`Generating transit data for scenario ${index}`), - (async () => { - const endPoint = browsochrones.pixelToOriginPoint( - Leaflet.CRS.EPSG3857.latLngToPoint(lonlat.toLeaflet(endLatlng), zoom), - zoom - ) - const data = await browsochrones.generateDestinationData({ - from: startLatlng || null, - to: { - ...endLatlng, - ...endPoint - } - }) - data.transitive.key = `${index}-${lonlat.toString(endLatlng)}` - return [setDestinationDataFor({data, index}), decrementWork()] - })() - ] -} - -export function updateSelectedTimeCutoff ({ - browsochrones, - latlng, - timeCutoff -}: { - browsochrones: Browsochrones, - latlng: LatLng, - timeCutoff: number -}) { - const actions = [setSelectedTimeCutoff(timeCutoff)] - - browsochrones.instances.map((instance, index) => { - if (instance.isLoaded()) { - actions.push( - generateIsochroneFor({ - browsochrones: instance, - index, - latlng, - timeCutoff - }) - ) - actions.push( - generateAccessiblityFor({ - browsochrones: instance, - index, - latlng, - timeCutoff - }) - ) - } - }) - - return actions -} - -/** - * What happens on end update: - * - Map marker is set to the new end point immmediately (if it wasn't a drag/drop) - * - If there's no label, the latlng point should be reverse geocoded and saved - * - If Browsochones is loaded, transitive data is generated - * - If Browsochones has a surface generated, travel time is calculated - */ -export function updateEnd ({ - browsochronesInstances, - startLatlng, - latlng, - label, - zoom -}: { - browsochronesInstances: Browsochrones[], - startLatlng: LatLng, - latlng: LatLng, - label: string, - zoom: number -}) { - const actions = [] - - // TODO: Remove this! - if (label && label.toLowerCase().indexOf('airport') !== -1) { - latlng = { - lat: 39.7146, - lng: -86.2983 - } - } - - if (label) { - actions.push(setEnd({label, latlng})) - } else { - actions.push( - setEnd({latlng}), - reverseGeocode({latlng}).then(({features}) => - setEndLabel(features[0].properties.label) - ) - ) - } - - browsochronesInstances.map((instance, index) => { - if (instance.isLoaded()) { - actions.push( - generateDestinationDataFor({ - browsochrones: instance, - startLatlng, - index, - endLatlng: latlng, - zoom - }) - ) - } - }) - - return actions -} diff --git a/src/actions/location.js b/src/actions/location.js new file mode 100644 index 0000000..fe5816e --- /dev/null +++ b/src/actions/location.js @@ -0,0 +1,65 @@ +// @flow +import {ACCESSIBILITY_IS_LOADING} from '../constants' + +import { + addActionLogItem, + clearIsochrone, + setEnd, + setOrigin, + setStart +} from './' +import {fetchDataForLonLat} from './data' +import {reverse as reverseGeocode} from './geocode' + +import type {Location} from '../types' + +/** + * Update the start + */ +export const updateStart = (value: Location) => + (dispatch: Dispatch, getState: any) => { + const state = getState() + const origins = state.data.origins + + dispatch([ + clearIsochrone(), + ...origins.map(o => + setOrigin({name: o.name, accessibility: ACCESSIBILITY_IS_LOADING})) + ]) + + if (value.label) { + dispatch([ + addActionLogItem(`Updating start to ${value.label}`), + setStart(value) + ]) + } else if (value.position) { + dispatch(reverseGeocode(value.position, (feature) => { + dispatch(setStart({ + position: value.position, + label: feature.place_name + })) + })) + } + + dispatch(fetchDataForLonLat(value.position)) + } + +/** + * Update the end point + */ +export const updateEnd = (value: Location) => + (dispatch: Dispatch, getState: any) => { + if (value.label) { + dispatch([ + addActionLogItem(`Updating end point to ${value.label}`), + setEnd(value) + ]) + } else { + dispatch(reverseGeocode(value.position, (feature) => { + dispatch(setEnd({ + position: value.position, + label: feature.place_name + })) + })) + } + } diff --git a/src/components/application.js b/src/components/application.js index 1c42139..393406d 100644 --- a/src/components/application.js +++ b/src/components/application.js @@ -1,5 +1,6 @@ // @flow import lonlat from '@conveyal/lonlat' +import message from '@conveyal/woonerf/message' import isEqual from 'lodash/isEqual' import memoize from 'lodash/memoize' import React, {Component} from 'react' @@ -8,15 +9,13 @@ import Form from './form' import Icon from './icon' import Log from './log' import Map from './map' -import messages from '../utils/messages' import RouteCard from './route-card' import type { - Accessibility, - BrowsochronesStore, Coordinate, GeocoderStore, LogItems, + LonLat, InputEvent, MapEvent, PointFeature, @@ -24,60 +23,68 @@ import type { UIStore } from '../types' +type Origin = { + name: string, + active: boolean +} + +type MapState = { + centerCoordinates: Coordinate, + tileUrl: string, + transitive: any, + travelTimes: any[], + waitTimes: any[], + zoom: number +} + type Props = { - accessibilityKeys: string[], + accessibility: number[][], actionLog: LogItems, - browsochrones: BrowsochronesStore, - destinations: Accessibility[], + data: { + grids: string[], + origins: Origin[] + }, geocoder: GeocoderStore, + isochrones: any[], journeys: any[], mapMarkers: any, - map: any, + map: MapState, pointsOfInterest: PointsOfInterest, showComparison: boolean, timeCutoff: any, ui: UIStore, - clearEnd(): void, clearIsochrone(): void, - clearStart(): void, - initializeBrowsochrones(any): void, - setActiveBrowsochronesInstance(number): void, - updateEnd(any): void, - updateStart(any): void, - updateSelectedTimeCutoff(any): void + initialize: () => void, + setActiveOrigin: (name: string) => void, + setEnd: (any) => void, + setSelectedTimeCutoff: (any) => void, + setStart: (any) => void, + updateEnd: (any) => void, + updateMap: (any) => void, + updateStart: (any) => void } type Marker = { position: Coordinate, label: string, - onDragEnd(MapEvent): void + onDragEnd: (MapEvent) => void } type State = { markers: Marker[] } -export default class Application extends Component { +export default class Application extends Component { + props: Props + state: State + state = { markers: this._createMarkersFromProps(this.props) } - constructor (props: Props) { - super(props) - const { - browsochrones, - initializeBrowsochrones, - geocoder, - map, - timeCutoff - } = props - initializeBrowsochrones({ - browsochrones, - geocoder, - map, - timeCutoff - }) + componentDidMount () { + this.props.initialize() } componentWillReceiveProps (nextProps: Props) { @@ -87,121 +94,96 @@ export default class Application extends Component { } _clearStartAndEnd = () => { - const {clearEnd, clearIsochrone, clearStart} = this.props - clearStart() + const {clearIsochrone, setEnd, setStart} = this.props + setStart(null) clearIsochrone() - clearEnd() + setEnd(null) } - _createMarkersFromProps (props: Props) { + _createMarkersFromProps (props: Props): Marker[] { const {mapMarkers} = props const markers = [] - if (mapMarkers.start && mapMarkers.start.latlng) { + if (mapMarkers.start && mapMarkers.start.position) { markers.push({ - position: mapMarkers.start.latlng, + position: mapMarkers.start.position, label: mapMarkers.start.label || '', onDragEnd: this._setStartWithEvent }) } - if (mapMarkers.end && mapMarkers.end.latlng) { + if (mapMarkers.end && mapMarkers.end.position) { markers.push({ - position: mapMarkers.end.latlng, + position: mapMarkers.end.position, label: mapMarkers.end.label || '', onDragEnd: this._setEndWithEvent }) } + return markers } - _setStart = ({label, latlng}: {label?: string, latlng: Coordinate}) => { - const {browsochrones, map, mapMarkers, timeCutoff, updateStart} = this.props - const endLatlng = mapMarkers.end && mapMarkers.end.latlng - ? mapMarkers.end.latlng - : null - - updateStart({ - browsochronesInstances: browsochrones.instances, - endLatlng, - label, - latlng: lonlat(latlng), - timeCutoff: timeCutoff.selected, - zoom: map.zoom - }) + _setStart = (opts: {label?: string, position: LonLat}) => { + this.props.updateStart(opts) } _setStartWithEvent = (event: MapEvent) => { - this._setStart({latlng: event.latlng || event.target._latlng}) + this.props.updateStart({position: lonlat(event.latlng || event.target._latlng)}) } - _setStartWithFeature = (feature: PointFeature) => { + _setStartWithFeature = (feature?: PointFeature) => { if (!feature) { this._clearStartAndEnd() } else { - const {geometry} = feature - - this._setStart({ + this.props.updateStart({ label: feature.properties.label, - latlng: geometry.coordinates + position: lonlat(feature.geometry.coordinates) }) } } - _setEnd = ({label, latlng}: {label?: string, latlng: Coordinate}) => { - const {browsochrones, map, mapMarkers, updateEnd} = this.props - updateEnd({ - browsochronesInstances: browsochrones.instances, - startLatlng: mapMarkers.start.latlng, - label, - latlng: lonlat(latlng), - zoom: map.zoom - }) + _setEnd = (opts: {label?: string, position: LonLat}) => { + this.props.updateEnd(opts) } _setEndWithEvent = (event: MapEvent) => { - this._setEnd({latlng: event.latlng || event.target._latlng}) + this.props.updateEnd({position: lonlat(event.latlng || event.target._latlng)}) } _setEndWithFeature = (feature: PointFeature) => { if (!feature) { - this.props.clearEnd() + this.props.setEnd(null) } else { const {geometry} = feature this._setEnd({ label: feature.properties.label, - latlng: geometry.coordinates + position: lonlat(geometry.coordinates) }) } } _onTimeCutoffChange = (event: InputEvent) => { - const {browsochrones, mapMarkers, updateSelectedTimeCutoff} = this.props - const timeCutoff = parseInt(event.currentTarget.value, 10) - updateSelectedTimeCutoff({ - browsochrones, - latlng: mapMarkers.start.latlng, - timeCutoff - }) + this.props.setSelectedTimeCutoff(parseInt(event.currentTarget.value, 10)) } - _setActiveBrowsochronesInstance = memoize(index => () => - this.props.setActiveBrowsochronesInstance(index)) + _setActiveOrigin = memoize(name => () => + this.props.setActiveOrigin(name)) count = 0 render () { const { - accessibilityKeys, + accessibility, actionLog, - browsochrones, - destinations, + data, geocoder, + isochrones, journeys, map, pointsOfInterest, showComparison, timeCutoff, - ui + ui, + updateMap } = this.props const {markers} = this.state @@ -209,15 +191,16 @@ export default class Application extends Component {
@@ -228,7 +211,7 @@ export default class Application extends Component { ? : } {' '} - {messages.Title} + {message('Title')}
{ selectedTimeCutoff={timeCutoff.selected} start={geocoder.start} /> - {destinations.map((accessibility, index) => ( + {data.origins.map((origin, index) => ( - {accessibility.name} + {origin.name} ))} {ui.showLog && @@ -264,7 +249,7 @@ export default class Application extends Component { actionLog.length > 0 &&
- {messages.Log.Title} + {message('Log.Title')}
} diff --git a/src/components/form.js b/src/components/form.js index 58b0e81..a937074 100644 --- a/src/components/form.js +++ b/src/components/form.js @@ -16,9 +16,9 @@ type Props = { boundary: GeocoderBoundary, end: null | Option, focusLatlng: LatLng, - onChangeEnd(PointFeature): void, - onChangeStart(PointFeature): void, - onTimeCutoffChange(InputEvent): void, + onChangeEnd: (PointFeature) => void, + onChangeStart: (PointFeature) => void, + onTimeCutoffChange: (InputEvent) => void, pointsOfInterest: PointsOfInterest, selectedTimeCutoff: number, start: null | Option diff --git a/src/components/map.js b/src/components/map.js index 139bf86..4501fdb 100644 --- a/src/components/map.js +++ b/src/components/map.js @@ -1,5 +1,7 @@ // @flow -import {Browser, LatLng} from 'leaflet' +import lonlat from '@conveyal/lonlat' +import message from '@conveyal/woonerf/message' +import {Browser} from 'leaflet' import React, {PureComponent} from 'react' import { GeoJson, @@ -13,13 +15,12 @@ import { import Icon from './icon' import {setKeyTo} from '../utils/hash' import leafletIcon from '../utils/leaflet-icons' -import messages from '../utils/messages' import TransitiveLayer from './transitive-map-layer' import transitiveStyle from '../transitive-style' -import type {Coordinate, Feature, MapEvent, PointsOfInterest} from '../types' +import type {Coordinate, Feature, LonLat, MapEvent, PointsOfInterest} from '../types' -const TILE_LAYER_URL = Browser.retina && process.env.LEAFLET_RETINA_URL +const TILE_URL = Browser.retina && process.env.LEAFLET_RETINA_URL ? process.env.LEAFLET_RETINA_URL : process.env.LEAFLET_TILE_URL @@ -34,63 +35,61 @@ const endIcon = leafletIcon({ }) type Props = { - active: number, centerCoordinates: Coordinate, - clearStartAndEnd(): void, + clearStartAndEnd: () => void, isochrones: any[], markers: any[], pointsOfInterest: PointsOfInterest, - setEnd(any): void, - setStart(any): void, + setEnd: (any) => void, + setStart: (any) => void, transitive: any, + updateMap: (any) => void, zoom: number } type State = { showSelectStartOrEnd: boolean, lastClickedLabel: null, - lastClickedLatlng: null | LatLng + lastClickedPosition: null | LonLat } -export default class Map extends PureComponent { +export default class Map extends PureComponent { + props: Props + state: State + state = { showSelectStartOrEnd: false, lastClickedLabel: null, - lastClickedLatlng: null + lastClickedPosition: null } _clearState (): void { this.setState({ showSelectStartOrEnd: false, lastClickedLabel: null, - lastClickedLatlng: null + lastClickedPosition: null }) } _clearStartAndEnd = (): void => { - const {clearStartAndEnd} = this.props - clearStartAndEnd() + this.props.clearStartAndEnd() this._clearState() } _onMapClick = (e: MapEvent): void => { this.setState({ showSelectStartOrEnd: !this.state.showSelectStartOrEnd, - lastClickedLatlng: e.latlng + lastClickedPosition: lonlat(e.latlng) }) } _setEnd = (): void => { - const {setEnd} = this.props - const {lastClickedLatlng} = this.state - setEnd({latlng: lastClickedLatlng}) + this.props.setEnd({position: this.state.lastClickedPosition}) this._clearState() } _setStart = (): void => { - const {setStart} = this.props - const {lastClickedLatlng} = this.state - setStart({latlng: lastClickedLatlng}) + this.props.setStart({position: this.state.lastClickedPosition}) this._clearState() } @@ -103,18 +102,19 @@ export default class Map extends PureComponent { const {coordinates} = feature.geometry this.setState({ lastClickedLabel: feature.properties.label, - lastClickedLatlng: {lat: coordinates[1], lng: coordinates[0]}, + lastClickedPosition: lonlat(coordinates), showSelectStartOrEnd: true }) } _setZoom = (e: MapEvent) => { - setKeyTo('zoom', e.target._zoom) + const zoom = e.target._zoom + this.props.updateMap({zoom}) + setKeyTo('zoom', zoom) } render (): React$Element { const { - active, centerCoordinates, isochrones, markers, @@ -124,7 +124,7 @@ export default class Map extends PureComponent { } = this.props const { lastClickedLabel, - lastClickedLatlng, + lastClickedPosition, showSelectStartOrEnd } = this.state const tileLayerProps = {} @@ -135,7 +135,7 @@ export default class Map extends PureComponent { } const baseIsochrone = isochrones[0] - const comparisonIsochrone = active !== 0 ? isochrones[active] : null + const comparisonIsochrone = null // active !== 0 ? isochrones[active] : null return ( { > @@ -166,7 +166,7 @@ export default class Map extends PureComponent { icon={index === 0 ? startIcon : endIcon} key={`marker-${index}`} onDragEnd={m.onDragEnd} - position={m.position} + position={lonlat.toLeaflet(m.position)} > {m.label && @@ -187,7 +187,7 @@ export default class Map extends PureComponent { } {showSelectStartOrEnd && - +
{lastClickedLabel &&

@@ -195,17 +195,17 @@ export default class Map extends PureComponent {

} {markers.length > 0 && } {markers.length > 0 && }
} diff --git a/src/components/route-card.js b/src/components/route-card.js index ad404ac..95504d5 100644 --- a/src/components/route-card.js +++ b/src/components/route-card.js @@ -2,16 +2,17 @@ import React from 'react' import toSpaceCase from 'lodash/lowerCase' -import {ACCESSIBILITY_IS_EMPTY, ACCESSIBILITY_IS_LOADING} from '../constants' import Icon from './icon' import messages from '../utils/messages' type Props = { active: boolean, alternate: boolean, - accessibility: any, - accessibilityKeys: string[], + accessibility: number[], children?: any, + grids: any[], + hasEnd: boolean, + hasStart: boolean, journeys: any[], oldAccessibility: any, oldTravelTime: number, @@ -25,8 +26,10 @@ export default ({ active, alternate, accessibility, - accessibilityKeys, children, + grids, + hasEnd, + hasStart, journeys, oldAccessibility, oldTravelTime, @@ -55,15 +58,21 @@ export default ({
- {showComparison - ? - : } - {accessibility !== ACCESSIBILITY_IS_EMPTY && - accessibility !== ACCESSIBILITY_IS_LOADING && +
+
+ {messages.Systems.AccessTitle} +
+ {hasStart + ? showComparison + ? + : + : {messages.Systems.SelectStart}} +
+ {hasStart && hasEnd && journeys && -
- {messages.Systems.AccessTitle} -
- {base === ACCESSIBILITY_IS_EMPTY - ? - {messages.Systems.SelectStart} - - : base === ACCESSIBILITY_IS_LOADING - ? - {messages.Systems.CalculatingAccessibility} - - : keys.map((k, i) => ( -
- - {(base[k] | 0).toLocaleString()} {' '} - {toSpaceCase(k)} -
- ))} + return
{grids.map((grid, i) => ( +
+ + {(accessibility[i] | 0).toLocaleString()} {' '} + {toSpaceCase(grid.name)}
- ) + ))}
} function AccessDiffPercentage ({newAccess, originalAccess}) { @@ -265,31 +257,16 @@ function AccessDiffPercentage ({newAccess, originalAccess}) { } } -function ShowDiff ({keys, base, comparison}) { - return ( -
-
- {messages.Systems.AccessTitle} -
- {base === ACCESSIBILITY_IS_EMPTY - ? - {messages.Systems.SelectStart} - - : base === ACCESSIBILITY_IS_LOADING - ? - {messages.Systems.CalculatingAccessibility} - - : keys.map((key, i) => ( -
- - {(base[key] | 0).toLocaleString()} {' '} - {toSpaceCase(key)} - -
- ))} +function ShowDiff ({accessibility, comparison, grids}) { + return
{grids.map((grid, i) => ( +
+ + {(accessibility[i] | 0).toLocaleString()} {' '} + {toSpaceCase(grid.name)} +
- ) + ))}
} diff --git a/src/constants.js b/src/constants.js index a77a5e3..9a24c92 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,3 +1,6 @@ // @flow export const ACCESSIBILITY_IS_EMPTY = 'accessibility-is-empty' export const ACCESSIBILITY_IS_LOADING = 'accessibility-is-loading' + +// URLS +export const MAPBOX_GEOCODING_URL = 'http://api.mapbox.com/geocoding/v5/mapbox.places' diff --git a/src/containers/application.js b/src/containers/application.js index f477414..915f17b 100644 --- a/src/containers/application.js +++ b/src/containers/application.js @@ -2,23 +2,28 @@ import {connect} from 'react-redux' import * as actions from '../actions' -import initializeBrowsochrones from '../actions/browsochrones' +import {initialize} from '../actions/data' +import * as locationActions from '../actions/location' import Application from '../components/application' -import selectAccessibilityKeys from '../selectors/accessibility-keys' -import selectJourneysFromTransitive from '../selectors/journeys-from-transitive' +import * as select from '../selectors' import selectPointsOfInterest from '../selectors/points-of-interest' import selectShowComparison from '../selectors/show-comparison' function mapStateToProps (state, ownProps) { return { ...state, - accessibilityKeys: selectAccessibilityKeys(state, ownProps), - journeys: selectJourneysFromTransitive(state, ownProps), + accessibility: select.accessibility(state, ownProps), + isochrones: select.isochrones(state, ownProps), + journeys: [], // selectJourneysFromTransitive(state, ownProps), pointsOfInterest: selectPointsOfInterest(state, ownProps), showComparison: selectShowComparison(state, ownProps) } } -export default connect(mapStateToProps, {...actions, initializeBrowsochrones})( +export default connect(mapStateToProps, { + ...actions, + ...locationActions, + initialize +})( Application ) diff --git a/src/index.js b/src/index.js index 94b9a04..76483f6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,23 @@ // @flow +import message from '@conveyal/woonerf/message' import mount from '@conveyal/woonerf/mount' +import React from 'react' import Application from './containers/application' import reducers from './reducers' -import messages from './utils/messages' // Set the title -document.title = messages.Title +document.title = message('Title') + +// Create an Application wrapper +function Wrapper ({history, store}) { + if (window) window.store = store + + return +} // Mount the app mount({ - app: Application, + app: Wrapper, reducers }) diff --git a/src/reducers/browsochrones.js b/src/reducers/browsochrones.js deleted file mode 100644 index 1f9a348..0000000 --- a/src/reducers/browsochrones.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import {handleActions} from 'redux-actions' - -export default handleActions( - { - 'set active browsochrones instance' (state, action) { - return { - ...state, - active: action.payload - } - }, - 'set browsochrones instances' (state, action) { - return { - ...state, - instances: action.payload - } - } - }, - {} -) diff --git a/src/reducers/data.js b/src/reducers/data.js new file mode 100644 index 0000000..60b2b16 --- /dev/null +++ b/src/reducers/data.js @@ -0,0 +1,52 @@ +// @flow +import {handleActions} from 'redux-actions' + +export default handleActions({ + 'set grid' (state, action) { + const grids = [...state.grids] + const gridIndex = grids.findIndex(g => g.name === action.payload.name) + + if (gridIndex > -1) { + grids[gridIndex] = {...grids[gridIndex], ...action.payload} + } else { + grids.push(action.payload) + } + + return { + ...state, + grids + } + }, + 'set origin' (state, action) { + const origins = [...state.origins] + const originIndex = origins.findIndex((o) => o.name === action.payload.name) + + if (originIndex > -1) { + origins[originIndex] = {...origins[originIndex], ...action.payload} + } else { + origins.push(action.payload) + } + + return { + ...state, + origins + } + }, + 'set active origin' (state, action) { + const origins = [...state.origins] + + return { + ...state, + origins: origins.map(o => o.name === action.payload ? {...o, active: true} : {...o, active: false}) + } + }, + 'set query' (state, action) { + return { + ...state, + query: action.payload + } + } +}, { + origins: [], + grids: [] +}) diff --git a/src/reducers/destinations.js b/src/reducers/destinations.js deleted file mode 100644 index d2ef51a..0000000 --- a/src/reducers/destinations.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import {handleActions} from 'redux-actions' - -import {ACCESSIBILITY_IS_EMPTY, ACCESSIBILITY_IS_LOADING} from '../constants' - -export default handleActions( - { - 'set accessibility for' (state, {payload}) { - const accessibility = [...state] - accessibility[payload.index] = { - accessibility: payload.accessibility, - name: payload.name - } - return accessibility - }, - 'set accessibility to empty for' (state, {payload}) { - const accessibility = [...state] - accessibility[payload.index] = { - accessibility: ACCESSIBILITY_IS_EMPTY, - name: payload.name - } - return accessibility - }, - 'set accessibility to loading for' (state, {payload}) { - const accessibility = [...state] - accessibility[payload.index] = { - accessibility: ACCESSIBILITY_IS_LOADING, - name: payload.name - } - return accessibility - }, - 'clear start' (state, action) { - return state.map(a => ({ - accessibility: ACCESSIBILITY_IS_EMPTY, - name: a.name - })) - } - }, - [] -) diff --git a/src/reducers/geocoder.js b/src/reducers/geocoder.js index 8793410..f08f5ec 100644 --- a/src/reducers/geocoder.js +++ b/src/reducers/geocoder.js @@ -6,67 +6,13 @@ export default handleActions( 'set start' (state, {payload}) { return { ...state, - start: { - label: payload.label, - value: payload.latlng - ? `${payload.latlng.lng},${payload.latlng.lat}` - : false - } - } - }, - 'set start label' (state, {payload}) { - if (payload) { - return { - ...state, - start: { - ...state.start, - label: payload - } - } - } else { - return { - ...state, - start: null - } + start: payload } }, 'set end' (state, {payload}) { return { ...state, - end: { - label: payload.label, - value: payload.latlng - ? `${payload.latlng.lng},${payload.latlng.lat}` - : false - } - } - }, - 'set end label' (state, {payload}) { - if (payload) { - return { - ...state, - end: { - ...state.end, - label: payload - } - } - } else { - return { - ...state, - end: null - } - } - }, - 'clear start' (state) { - return { - ...state, - start: null - } - }, - 'clear end' (state) { - return { - ...state, - end: null + end: payload } } }, diff --git a/src/reducers/index.js b/src/reducers/index.js index b29920b..45048f5 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,7 +1,6 @@ // @flow import actionLog from './action-log' -import browsochrones from './browsochrones' -import destinations from './destinations' +import data from './data' import geocoder from './geocoder' import map from './map' import mapMarkers from './map-marker' @@ -10,8 +9,7 @@ import ui from './ui' export default { actionLog, - browsochrones, - destinations, + data, geocoder, map, mapMarkers, diff --git a/src/reducers/map.js b/src/reducers/map.js index 2e6e070..b5a3dd9 100644 --- a/src/reducers/map.js +++ b/src/reducers/map.js @@ -53,15 +53,6 @@ export default handleActions( waitTimes } }, - 'set active browsochrones instance' (state, action) { - const index = action.payload - return { - ...state, - active: index, - geojson: [state.isochrones[index]], - transitive: state.transitives[index] - } - }, 'clear start' (state, action) { return { ...state, diff --git a/src/selectors/accessibility-keys.js b/src/selectors/accessibility-keys.js deleted file mode 100644 index c3b3258..0000000 --- a/src/selectors/accessibility-keys.js +++ /dev/null @@ -1,6 +0,0 @@ -import {createSelector} from 'reselect' - -export default createSelector( - state => state.destinations[0], - (accessibility = {}) => Object.keys(accessibility.accessibility || {}) -) diff --git a/src/selectors/accessibility.js b/src/selectors/accessibility.js new file mode 100644 index 0000000..664cd4f --- /dev/null +++ b/src/selectors/accessibility.js @@ -0,0 +1,18 @@ +// @flow +import get from 'lodash/get' +import {createSelector} from 'reselect' + +import selectTravelTimeSurfaces from './travel-time-surfaces' +import accessibilityForGrid from '../utils/accessibility-for-grid' + +export default createSelector( + selectTravelTimeSurfaces, + (state) => get(state, 'data.grids'), + (state) => get(state, 'data.query'), + (state) => get(state, 'timeCutoff.selected'), + (surfaces, grids, query, cutoff) => + surfaces.map(surface => grids.map(grid => surface && surface.data && grid.data && query + ? accessibilityForGrid({surface: surface.data, grid, query, cutoff}) + : -1 + )) +) diff --git a/src/selectors/grids.js b/src/selectors/grids.js new file mode 100644 index 0000000..46e7f7c --- /dev/null +++ b/src/selectors/grids.js @@ -0,0 +1 @@ +// @flow diff --git a/src/selectors/index.js b/src/selectors/index.js new file mode 100644 index 0000000..5b218cf --- /dev/null +++ b/src/selectors/index.js @@ -0,0 +1,3 @@ +// @flow +export {default as accessibility} from './accessibility' +export {default as isochrones} from './isochrones' diff --git a/src/selectors/isochrones.js b/src/selectors/isochrones.js new file mode 100644 index 0000000..4811861 --- /dev/null +++ b/src/selectors/isochrones.js @@ -0,0 +1,39 @@ +// @flow +import jsolines from 'jsolines' +import {Map as LeafletMap} from 'leaflet' +import get from 'lodash/get' +import memoize from 'lodash/memoize' +import {createSelector} from 'reselect' +import uuid from 'uuid' + +export default createSelector( + (state) => get(state, 'data.origins'), + (state) => get(state, 'timeCutoff.selected'), + (origins = [], timeCutoff) => origins.map((origin, index) => { + if (origin.travelTimeSurface && origin.travelTimeSurface.data) { + return getIsochrone(origin, index, timeCutoff) + } + }) +) + +/** + * Create an isochrone. Save results based on the origin and timecutoff. + */ +const getIsochrone = memoize((origin, index, timeCutoff) => { + const surface = origin.travelTimeSurface + const isochrone = jsolines({ + ...surface, // height, width, surface + surface: surface.data, + cutoff: timeCutoff, + project ([x, y]) { + const {lat, lng} = LeafletMap.prototype.unproject( + [x + surface.west, y + surface.north], + surface.zoom + ) + return [lng, lat] + } + }) + + // create the uuid here so that if + return {...isochrone, key: uuid.v4()} +}, (o, i, c) => `${o.name}-${i}-${o.originPoint.x}-${o.originPoint.y}-${c}`) diff --git a/src/selectors/journeys-from-transitive.js b/src/selectors/journeys-from-transitive.js index 9d0bff9..7b6d80f 100644 --- a/src/selectors/journeys-from-transitive.js +++ b/src/selectors/journeys-from-transitive.js @@ -1,11 +1,12 @@ // @flow import Color from 'color' import toCapitalCase from 'lodash/capitalize' +import get from 'lodash/get' import unique from 'lodash/uniq' import {createSelector} from 'reselect' export default createSelector( - state => state.map.transitives, + state => get(state, 'data.query.transitiveData'), (transitiveData = []) => transitiveData.map(extractRelevantTransitiveInfo) ) diff --git a/src/selectors/show-comparison.js b/src/selectors/show-comparison.js index 7412658..9f0485f 100644 --- a/src/selectors/show-comparison.js +++ b/src/selectors/show-comparison.js @@ -1,6 +1,8 @@ +// @flow +import get from 'lodash/get' import {createSelector} from 'reselect' export default createSelector( - state => state.destinations[0], - (accessibility = {}) => Object.keys(accessibility).length > 0 + state => get(state, 'data.origins'), + (origins) => origins.length > 1 ) diff --git a/src/selectors/travel-time-surfaces.js b/src/selectors/travel-time-surfaces.js new file mode 100644 index 0000000..ebcf5f4 --- /dev/null +++ b/src/selectors/travel-time-surfaces.js @@ -0,0 +1,8 @@ +// @flow +import get from 'lodash/get' +import {createSelector} from 'reselect' + +export default createSelector( + (state) => get(state, 'data.origins'), + (origins) => origins.map(o => o.travelTimeSurface) +) diff --git a/src/types.js b/src/types.js index 4f4b384..dc78600 100644 --- a/src/types.js +++ b/src/types.js @@ -1,18 +1,38 @@ // @flow - -import Browsochrones from 'browsochrones' - -/** - * Simple types - */ export type LatLng = { lat: number, lng: number } +export type LonLat = {lon: number, lat: number} + +export type Location = { + label: string, + position: LonLat +} + export type Coordinate = [number, number] export type Coordinates = Coordinate[] +export type Grid = { + contains: (number, number) => boolean, + valueAtPoint: (number, number) => number, + data: Int32Array, + north: number, + west: number, + height: number, + width: number, + zoom: number +} + +export type Query = { + height: number, + width: number, + north: number, + west: number, + zoom: number +} + /** * GeoJSON */ @@ -54,25 +74,11 @@ export type LogItem = { export type LogItems = LogItem[] -export type BrowsochronesStore = { - active: number, - instances: Browsochrones[], - origins: Array<{ - name: string, - url: string - }>, - grids: string[], - gridsUrl: string -} - -export type Accessibility = { - name: string, - accessibility: | 'accessibility-is-empty' - | 'accessibility-is-loading' - | { - [key: string]: number - } -} +export type Accessibility = 'accessibility-is-empty' + | 'accessibility-is-loading' + | { + [key: string]: number + } export type Option = { label: string, @@ -110,8 +116,6 @@ export type UIStore = { export type Store = { actionLog: LogItems, - browsochrones: BrowsochronesStore, - destinations: Accessibility[], geocoder: GeocoderStore, map: any, mapMarkers: any, diff --git a/src/utils/accessibility-for-grid.js b/src/utils/accessibility-for-grid.js new file mode 100644 index 0000000..9ec8420 --- /dev/null +++ b/src/utils/accessibility-for-grid.js @@ -0,0 +1,62 @@ +// @flow +import type {Grid, Query} from '../types' + +/** + * Get the cumulative accessibility number for a cutoff from a travel time + * surface. This function always calculates _average_ accessibility. Calculating + * best or worst case accessibility is computationally complex because you must + * individually calculate accessibility for every minute, save all of those + * values, and then take a minumum. (Saving the worst-case travel time to each + * pixel allows you to calculate a bound, but does not allow calculation of the + * true minimum, because it is possible that all the worst-case travel times + * cannot appear simultaneously. Of course this comes back to the definition of + * your measure, and how fungible you consider opportunities to be.) + * + * The cutoff used is the cutoff that was specified in the surface generation. + * If you want a different cutoff you must regenerate the surface. The reason + * for this is that we need to know at every minute whether each destination was + * reached within a certain amount of time. Storing this for every possible + * cutoff is not feasible (the data become too large), so we only store it for a + * single cutoff during surface generation. However, calculating accessibility + * for additional grids should only take milliseconds. + * + * TODO in OTP/R5 we have a sigmoidal cutoff here to avoid "echoes" of high + * density locations at 60 minutes travel time from their origins. But maybe + * also we just want to not represent these things as hard edges since + * accessibility is a continuous phenomenon. No one is saying "ah, rats, it + * takes 60 minutes and 10 seconds to get work, I have to find a job that's 20 + * meters closer to home..." + * + * @param {Number} cutoff + * @param {Grid} grid + * @param {Query} query + * @param {Uint8Array} surface + * @returns {Number} accessibility + */ +export default function accessibilityForGrid ({ + cutoff = 60, + grid, + query, + surface +}: { + cutoff: number, + grid: Grid, + query: Query, + surface: Uint8Array +}): number { + let accessibility = 0 + for (let y = 0, pixel = 0; y < query.height; y++) { + for (let x = 0; x < query.width; x++, pixel++) { + const travelTime = surface[pixel] + + // ignore unreached locations + if (travelTime <= cutoff) { + const gridx = x + query.west - grid.west + const gridy = y + query.north - grid.north + accessibility += grid.valueAtPoint(gridx, gridy) + } + } + } + + return accessibility +} diff --git a/yarn.lock b/yarn.lock index ec18564..3dbb530 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,22 +6,21 @@ version "1.3.0" resolved "https://registry.yarnpkg.com/@conveyal/lonlat/-/lonlat-1.3.0.tgz#3fe586ee21a9159052156959bc3c3eaa22f056f5" -"@conveyal/woonerf@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@conveyal/woonerf/-/woonerf-2.3.0.tgz#fbce0ed830548fc8a5806934ce08da36c0b9aec1" +"@conveyal/woonerf@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@conveyal/woonerf/-/woonerf-3.0.0.tgz#8575eab098c696e43423633f30bb9640eb5186a1" dependencies: - auth0-lock "^10.10.2" + auth0-lock "^10.19.0" + debug "^2.6.8" isomorphic-fetch "^2.2.1" - lodash.isequal "^4.5.0" - lodash.isobject "^3.0.2" - lodash.merge "^4.6.0" + lodash "^4.17.4" react-addons-perf "^15.4.2" - react-redux "^5.0.2" + react-redux "^5.0.5" react-router "^3.0.2" - react-router-redux "^4.0.7" - redux "^3.6.0" - redux-actions "^2.0.2" - redux-logger "^3.0.1" + react-router-redux "^4.0.8" + redux "^3.7.2" + redux-actions "^2.2.1" + redux-logger "^3.0.6" redux-thunk "^2.2.0" "@semantic-release/commit-analyzer@^2.0.0": @@ -57,6 +56,20 @@ conventional-changelog "0.0.17" github-url-from-git "^1.4.0" +"@turf/helpers@^3.10.3": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-3.13.0.tgz#d06078a1464cf56cdb7ea624ea1e13a71b88b806" + +"@turf/inside@^3.10.3": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@turf/inside/-/inside-3.14.0.tgz#d6b6af55882cbdb8f9a558dca98689c67bd3c590" + dependencies: + "@turf/invariant" "^3.13.0" + +"@turf/invariant@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-3.13.0.tgz#89243308cd563206e81e5c6162e0d22f61822f90" + JSONStream@^1.0.3: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -331,30 +344,32 @@ augment@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/augment/-/augment-4.3.0.tgz#7dd446264d195ef5efa8b3fe0f89a8f29a160a43" -auth0-js@~8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-8.3.0.tgz#1e7f939e3bd267b6a5fb87614255af4265a646f6" +auth0-js@~8.12.1: + version "8.12.2" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-8.12.2.tgz#ab46cd180f9500de3d68610f49432f674f9dc5cd" dependencies: base64-js "^1.2.0" - idtoken-verifier "^1.0.1" + idtoken-verifier "^1.1.0" + qs "^6.4.0" superagent "^3.3.1" url-join "^1.1.0" winchan "^0.2.0" -auth0-lock@^10.10.2: - version "10.12.3" - resolved "https://registry.yarnpkg.com/auth0-lock/-/auth0-lock-10.12.3.tgz#b3cb2339e8327eac890a5e0774a20b8c2559ef4e" +auth0-lock@^10.19.0: + version "10.24.3" + resolved "https://registry.yarnpkg.com/auth0-lock/-/auth0-lock-10.24.3.tgz#335405cb48c9412229639a37fef357416b0caa4e" dependencies: - auth0-js "~8.3.0" + auth0-js "~8.12.1" blueimp-md5 "2.3.1" fbjs "^0.3.1" idtoken-verifier "^1.0.1" immutable "^3.7.3" jsonp "^0.2.0" password-sheriff "^1.1.0" - react "^15.0.0 || ^16.0.0" - react-addons-css-transition-group "^15.0.0 || ^16.0.0" - react-dom "^15.0.0 || ^16.0.0" + prop-types "^15.6.0" + react "^15.6.2" + react-dom "^15.6.2" + react-transition-group "^2.2.1" superagent "^3.3.1" trim "0.0.1" url-join "^1.1.0" @@ -1441,19 +1456,6 @@ browserslist@^1.0.0, browserslist@^1.3.6, browserslist@^1.4.0, browserslist@^1.7 caniuse-db "^1.0.30000631" electron-to-chromium "^1.2.5" -browsochrones@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/browsochrones/-/browsochrones-0.9.1.tgz#38f688bbc1a180cb461fb8650e30afea1f041b22" - dependencies: - color "^0.11.3" - debug "^2.2.0" - jsolines "^0.2.2" - leaflet "^0.7.7" - lodash.fill "^3.3.2" - lodash.slice "^4.0.2" - lonlng "^0.2.0" - web-worker-promise-interface "^0.2.0" - bser@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169" @@ -1611,6 +1613,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1681,7 +1687,7 @@ circular-json@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" -classnames@^2.2.4: +classnames@^2.2.4, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" @@ -1915,6 +1921,10 @@ cookiejar@^2.0.6: version "2.1.0" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.0.tgz#86549689539b6d0e269b6637a304be508194d898" +cookiejar@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -1962,6 +1972,14 @@ create-react-class@^15.5.3: loose-envify "^1.3.1" object-assign "^4.1.1" +create-react-class@^15.6.0: + version "15.6.2" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn-async@^2.1.1: version "2.2.5" resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" @@ -2125,6 +2143,18 @@ debug@2.6.1: dependencies: ms "0.7.2" +debug@^2.6.1: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2273,6 +2303,10 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -2815,6 +2849,18 @@ fbjs@^0.3.1: ua-parser-js "^0.7.9" whatwg-fetch "^0.9.0" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.9" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14" @@ -2957,6 +3003,14 @@ form-data@^2.1.1, form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + form-data@~1.0.0-rc4: version "1.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" @@ -3419,6 +3473,16 @@ idtoken-verifier@^1.0.1: superagent "^3.3.1" url-join "^1.1.0" +idtoken-verifier@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-1.1.1.tgz#bc6eb46e6a153bb2058aed1cac2c27ac6751b5e5" + dependencies: + base64-js "^1.2.0" + crypto-js "^3.1.9-1" + jsbn "^0.1.0" + superagent "^3.8.2" + url-join "^1.1.0" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -4092,12 +4156,13 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" -jsolines@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/jsolines/-/jsolines-0.2.3.tgz#0b1a6ed074e2dff49a44b0573bd5848b409de064" +jsolines@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jsolines/-/jsolines-1.0.2.tgz#5332f7d77f5640054471a134317ae85346118c34" dependencies: - turf-inside "^3.0.12" - turf-point "^2.0.1" + "@turf/helpers" "^3.10.3" + "@turf/inside" "^3.10.3" + debug "^2.6.1" json-schema@0.2.3: version "0.2.3" @@ -4309,10 +4374,6 @@ lodash.defaults@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" -lodash.fill@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/lodash.fill/-/lodash.fill-3.4.0.tgz#a3c74ae640d053adf0dc2079f8720788e8bfef85" - lodash.filter@^4.4.0, lodash.filter@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" @@ -4333,18 +4394,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - lodash.isnil@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -4369,7 +4422,7 @@ lodash.memoize@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" -lodash.merge@^4.4.0, lodash.merge@^4.6.0: +lodash.merge@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" @@ -4409,10 +4462,6 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" -lodash.slice@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.slice/-/lodash.slice-4.2.0.tgz#85fb9d49223c64d9d5890f32322777c7416cac27" - lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -4464,10 +4513,6 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -lonlng@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lonlng/-/lonlng-0.2.0.tgz#2137c2b2426535738f5994d046f316a7076d62dc" - loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" @@ -4661,6 +4706,10 @@ mime@1.3.4, mime@^1.2.11, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + minimalistic-assert@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" @@ -5635,6 +5684,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@~15.5.7: fbjs "^0.8.9" loose-envify "^1.3.1" +prop-types@^15.5.8, prop-types@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -5673,6 +5730,10 @@ qs@^6.1.0, qs@^6.3.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@^6.4.0, qs@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + qs@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" @@ -5728,13 +5789,6 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" -"react-addons-css-transition-group@^15.0.0 || ^16.0.0": - version "15.4.2" - resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.4.2.tgz#b7828834dfa14229fe07750e331e8a8cb6fb7745" - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" - react-addons-perf@^15.4.2: version "15.4.2" resolved "https://registry.yarnpkg.com/react-addons-perf/-/react-addons-perf-15.4.2.tgz#110bdcf5c459c4f77cb85ed634bcd3397536383b" @@ -5749,7 +5803,7 @@ react-addons-test-utils@^15.5.1: fbjs "^0.8.4" object-assign "^4.1.0" -"react-dom@^15.0.0 || ^16.0.0", react-dom@^15.5.4: +react-dom@^15.5.4: version "15.5.4" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" dependencies: @@ -5758,6 +5812,15 @@ react-addons-test-utils@^15.5.1: object-assign "^4.1.0" prop-types "~15.5.7" +react-dom@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react-input-autosize@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.1.0.tgz#3fe1ac832387d8abab85f6051ceab1c9e5570853" @@ -5773,7 +5836,7 @@ react-pure-render@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-pure-render/-/react-pure-render-1.0.2.tgz#9d8a928c7f2c37513c2d064e57b3e3c356e9fabb" -react-redux@^5.0.2, react-redux@^5.0.5: +react-redux@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.5.tgz#f8e8c7b239422576e52d6b7db06439469be9846a" dependencies: @@ -5785,7 +5848,7 @@ react-redux@^5.0.2, react-redux@^5.0.5: loose-envify "^1.1.0" prop-types "^15.5.10" -react-router-redux@^4.0.7: +react-router-redux@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" @@ -5816,7 +5879,18 @@ react-select@^1.0.0-rc.3: classnames "^2.2.4" react-input-autosize "^1.1.0" -"react@^15.0.0 || ^16.0.0", react@^15.5.4: +react-transition-group@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" + dependencies: + chain-function "^1.0.0" + classnames "^2.2.5" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.8" + warning "^3.0.0" + +react@^15.5.4: version "15.5.4" resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047" dependencies: @@ -5825,6 +5899,16 @@ react-select@^1.0.0-rc.3: object-assign "^4.1.0" prop-types "^15.5.7" +react@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -5944,7 +6028,7 @@ reduce-reducers@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" -redux-actions@^2.0.2, redux-actions@^2.0.3: +redux-actions@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.0.3.tgz#1550aba9def179166ccd234d07672104a736d889" dependencies: @@ -5953,7 +6037,16 @@ redux-actions@^2.0.2, redux-actions@^2.0.3: lodash-es "^4.17.4" reduce-reducers "^0.1.0" -redux-logger@^3.0.1: +redux-actions@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.2.1.tgz#d64186b25649a13c05478547d7cd7537b892410d" + dependencies: + invariant "^2.2.1" + lodash "^4.13.1" + lodash-es "^4.17.4" + reduce-reducers "^0.1.0" + +redux-logger@^3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" dependencies: @@ -5976,6 +6069,15 @@ redux@^3.6.0: loose-envify "^1.1.0" symbol-observable "^1.0.2" +redux@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" @@ -6668,6 +6770,21 @@ superagent@^3.3.1: qs "^6.1.0" readable-stream "^2.0.5" +superagent@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + supports-color@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.3.1.tgz#15758df09d8ff3b4acc307539fabe27095e1042d" @@ -6690,6 +6807,10 @@ symbol-observable@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" +symbol-observable@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" + symbol-tree@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -6894,22 +7015,6 @@ tunnel-agent@~0.4.1: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" -turf-inside@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-inside/-/turf-inside-3.0.12.tgz#9ba40fa6eed63bec7e7d88aa6427622c4df07066" - dependencies: - turf-invariant "^3.0.12" - -turf-invariant@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" - -turf-point@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/turf-point/-/turf-point-2.0.1.tgz#a2dcc30a2d20f44cf5c6271df7bae2c0e2146069" - dependencies: - minimist "^1.1.0" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -7045,7 +7150,7 @@ uuid@3.0.1, uuid@^3.0.0, uuid@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" -uuid@^2.0.1, uuid@^2.0.3: +uuid@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -7117,13 +7222,6 @@ watchify@^3.3.1, watchify@^3.9.0: through2 "^2.0.0" xtend "^4.0.0" -web-worker-promise-interface@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/web-worker-promise-interface/-/web-worker-promise-interface-0.2.0.tgz#7b12ba789cef89a833f263d30bb1d3fdf60d4a70" - dependencies: - uuid "^2.0.1" - webworkify "^1.4.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -7142,10 +7240,6 @@ websocket-extensions@>=0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" -webworkify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.4.0.tgz#71245d1e34cacf54e426bd955f8cc6ee12d024c2" - whatwg-encoding@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4"