diff --git a/src/components/application.js b/src/components/application.js index c3b73e9..47951ae 100644 --- a/src/components/application.js +++ b/src/components/application.js @@ -18,6 +18,7 @@ import type { UIStore } from '../types' import {getAsObject} from '../utils/hash' +import downloadJson from '../utils/download-json' import Form from './form' import Gridualizer from './gridualizer' @@ -77,15 +78,16 @@ type State = { componentError: any } -const getStyle = color => ({ +const getStyle = color => () => ({ fillColor: color, + fillOpacity: 0.4, pointerEvents: 'none', stroke: color, weight: 1 }) const BASE_ISOCHRONE_STYLE = getStyle('#4269a4') -const COMP_ISOCHRONE_STYLE = getStyle('darkorange') +const COMP_ISOCHRONE_STYLE = getStyle('#ff8c00') /** * @@ -196,6 +198,21 @@ export default class Application extends Component { _setActiveNetwork = memoize(name => () => this.props.setActiveNetwork(name)) + _downloadIsochrone = memoize(index => () => { + const p = this.props + const isochrone = p.isochrones[index] + if (isochrone) { + const name = p.data.networks[index].name + const ll = lonlat.toString(p.geocoder.start.position) + downloadJson({ + data: isochrone, + filename: `${name}-${ll}-${p.timeCutoff.selected}min-isochrone.json` + }) + } else { + window.alert('No isochrone has been generated for this network.') + } + }) + /** * */ @@ -245,16 +262,21 @@ export default class Application extends Component { {drawActiveOpportunityDataset && } - {!isLoading && -
- {isochrones[0] && - } + {!isLoading && isochrones[0] && + } - {comparisonIsochrone && - } + {!isLoading && comparisonIsochrone && + } - -
} + {!isLoading && } 0} componentError={componentError}> @@ -274,9 +296,10 @@ export default class Application extends Component { {!isLoading && @@ -304,14 +327,15 @@ export default class Application extends Component { } {ui.allowChangeConfig && diff --git a/src/components/geojson.js b/src/components/geojson.js index 0aa302e..acfc3bf 100644 --- a/src/components/geojson.js +++ b/src/components/geojson.js @@ -12,4 +12,3 @@ export default class GeoJSON extends MapLayer { }) } } - diff --git a/src/components/icon.js b/src/components/icon.js index 74b0de6..42c42f4 100644 --- a/src/components/icon.js +++ b/src/components/icon.js @@ -4,4 +4,3 @@ import React from 'react' export default function Icon ({type}) { return } - diff --git a/src/components/map.js b/src/components/map.js index d6c5b36..260de63 100644 --- a/src/components/map.js +++ b/src/components/map.js @@ -27,6 +27,8 @@ const TILE_URL = Leaflet.Browser.retina && process.env.LEAFLET_RETINA_URL ? process.env.LEAFLET_RETINA_URL : process.env.LEAFLET_TILE_URL +const LABEL_URL = process.env.LABEL_URL + const TILE_LAYER_PROPS = {} if (Leaflet.Browser.retina) { TILE_LAYER_PROPS.tileSize = 512 @@ -177,18 +179,23 @@ export default class Map extends PureComponent { onZoomend={this._setZoom} zoom={zoom} onClick={this._onMapClick} - preferCanvas zoomControl={false} > {children} + {LABEL_URL && + } + {(!start || !end) && pointsOfInterest.length > 0 && { active, alternate, children, - onClick, + downloadIsochrone, + showIsochrone, title } = this.props return ( @@ -28,17 +28,25 @@ export default class RouteCard extends React.PureComponent { (active ? ' Card-active' : '') } > - {title} - - {active ? : message('Systems.Show')} - - + + {active ? : } + + + + + {children}
) diff --git a/src/components/route-segments.js b/src/components/route-segments.js index 05de5cc..10f066a 100644 --- a/src/components/route-segments.js +++ b/src/components/route-segments.js @@ -14,7 +14,7 @@ export default function RouteSegments ({routeSegments, oldTravelTime, travelTime return ( - + Take {bestJourney.map((segment, index) => ( @@ -55,6 +55,7 @@ const Segment = ({segment}) => ( backgroundColor: segment.backgroundColor || 'inherit', color: segment.color || 'inherit' }} + title={segment.name} > {segment.name} diff --git a/src/index.css b/src/index.css index 7ffdbbb..4d55c57 100644 --- a/src/index.css +++ b/src/index.css @@ -155,20 +155,23 @@ html, body { } .Card-alternate .CardTitle { - background-color: orange; + background-color: #ff8c00; } .CardTitle { background-color: #4269a4; color: rgba(255, 255, 255); - padding: 0.5rem 1rem; + padding: 1rem 0.5rem 1rem 1rem; font-weight: var(--bold); +} + +.CardTitle > a { cursor: pointer; - display: block; + padding: 0 0.5rem; } -.CardTitle:focus { - outline: none; +.CardTitle > a:hover { + color: #D0021B; } .Card-active > .CardTitle { @@ -193,20 +196,21 @@ html, body { } table.CardContent { - padding: 0; - border-spacing: 1rem 0.75rem; + padding: 0.25rem 0; + border-spacing: 1rem 0.5rem; + line-height: 1.5rem; } table.CardContent td { vertical-align: top; } -.CardContent .alert { - color: #c92336; +table.CardContent td:first-of-type { + text-align: center; } -.BestTrip { - margin-bottom: 1rem; +.CardContent .alert { + color: #c92336; } .Attribution { @@ -227,10 +231,15 @@ table.CardContent td { .CardSegment { font-size: 0.75rem; - padding: 0.25rem; + padding: 0 0.25rem; border-radius: var(--rad); margin-right: 0.25rem; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40px; + margin-bottom: -6px; + display: inline-block; } .Card .increase { @@ -241,11 +250,6 @@ table.CardContent td { color: red; } -.AlternateTrips { - margin: 0 0 0.75em; - line-height: 1.5em; -} - .leaflet-popup-content-wrapper, .map-legends, .map-tooltip { @@ -335,11 +339,11 @@ table.CardContent td { } .LeafletIcon.End { - background-color: orange; + background-color: #ff8c00; } .LeafletIcon.End:before { - border-top: 20px solid orange; + border-top: 20px solid #ff8c00; } .LeafletIcon.End:after { diff --git a/src/selectors/active-opportunity-dataset.js b/src/selectors/active-opportunity-dataset.js index a51ba67..e8f346e 100644 --- a/src/selectors/active-opportunity-dataset.js +++ b/src/selectors/active-opportunity-dataset.js @@ -5,4 +5,3 @@ export default createSelector( (state) => state.data.grids, (grids) => grids[0] ) - diff --git a/src/selectors/draw-active-opportunity-dataset.js b/src/selectors/draw-active-opportunity-dataset.js index b66b6a1..e9d0af0 100644 --- a/src/selectors/draw-active-opportunity-dataset.js +++ b/src/selectors/draw-active-opportunity-dataset.js @@ -13,4 +13,3 @@ export default createSelector( interpolator: gridualizer.interpolators.bicubic }) ) - diff --git a/src/selectors/draw-isochrones.js b/src/selectors/draw-isochrones.js new file mode 100644 index 0000000..91025ac --- /dev/null +++ b/src/selectors/draw-isochrones.js @@ -0,0 +1,30 @@ +// @flow +import gdlz from '@conveyal/gridualizer' +import {createSelector} from 'reselect' +import get from 'lodash/get' + +const colors = [ + 'rgba(0, 0, 0, 0.0)', + 'rgba(66, 105, 164, 0.2)', + 'rgba(66, 105, 164, 0.4)', + 'rgba(66, 105, 164, 0.6)', + 'rgba(66, 105, 164, 0.8)' +] + +colors.reverse() + +export default createSelector( + state => get(state, 'data.networks'), + (networks = []) => + networks.map(n => { + if (n.travelTimeSurface && n.travelTimeSurface.data) { + const eq = gdlz.classifiers.equal() + const breaks = eq({min: 0, max: 120}, colors.length) + return gdlz.createDrawTile({ + colorizer: gdlz.colorizers.choropleth(breaks, colors), + grid: n.travelTimeSurface, + interpolator: gdlz.interpolators.spline + }) + } + }) +) diff --git a/src/selectors/index.js b/src/selectors/index.js index 37ec1a5..fba120f 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -4,6 +4,7 @@ export {default as activeNetworkIndex} from './active-network-index' export {default as activeTransitive} from './active-transitive' export {default as allTransitiveData} from './all-transitive-data' export {default as drawActiveOpportunityDataset} from './draw-active-opportunity-dataset' +export {default as drawIsochrones} from './draw-isochrones' export {default as isochrones} from './isochrones' export {default as loading} from './loading' export {default as pointsOfInterest} from './points-of-interest' diff --git a/src/selectors/isochrones.js b/src/selectors/isochrones.js index d341a61..f9c7e08 100644 --- a/src/selectors/isochrones.js +++ b/src/selectors/isochrones.js @@ -2,13 +2,13 @@ import jsolines from 'jsolines' import {Map as LeafletMap} from 'leaflet' import get from 'lodash/get' -import memoize from 'lodash/memoize' import {createSelector} from 'reselect' export default createSelector( state => get(state, 'data.networks'), state => get(state, 'timeCutoff.selected'), - (networks = [], timeCutoff) => + state => state.map, + (networks = [], timeCutoff, mapData) => networks.map((network, index) => { if (network.travelTimeSurface && network.travelTimeSurface.data) { return getIsochrone(network, index, timeCutoff) @@ -26,7 +26,7 @@ function toKey (n, i, c) { /** * Create an isochrone. Save results based on the network and timecutoff. */ -const getIsochrone = memoize((network, index, timeCutoff) => { +const getIsochrone = (network, index, timeCutoff) => { const surface = network.travelTimeSurface const isochrone = jsolines({ ...surface, // height, width, surface @@ -43,4 +43,4 @@ const getIsochrone = memoize((network, index, timeCutoff) => { // create the key for react-leaflet/GeoJSON here return {...isochrone, key: toKey(network, index, timeCutoff)} -}, toKey) +} diff --git a/src/utils/download-json.js b/src/utils/download-json.js new file mode 100644 index 0000000..aa86fb6 --- /dev/null +++ b/src/utils/download-json.js @@ -0,0 +1,21 @@ +// @flow +export default function downloadObjectAsJson ({ + data, + filename +}: { + data: Object, + filename: string +}) { + try { + const out = JSON.stringify(data, null, '\t') + const uri = `data:application/json;base64,${window.btoa(out)}` + const a = document.createElement('a') + a.setAttribute('href', uri) + a.setAttribute('target', '_blank') + a.setAttribute('download', filename) + a.click() + } catch (e) { + window.alert(`Can not download filename:\n${e.message}`) + throw e + } +}