diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 9030fa631..612b1c1b7 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -304,38 +304,184 @@ exports[`components > viewers > stop viewer should render countdown times after Plan a trip: - - - - - - - - | - - - - - - + + + + + + + + + + + + From Location Icon + + + + + + + + + + + + + + + + + + + + + + + To Location Icon + + + + + + + + + + + + + + + +
viewers > stop viewer should render countdown times for st Plan a trip: - - - - - - - - | - - - - - - + + + + + + + + + + + + From Location Icon + + + + + + + + + + + + + + + + + + + + + + + To Location Icon + + + + + + + + + + + + + + + +
viewers > stop viewer should render times after midnight w Plan a trip: - - - - - - - - | - - - - - - + + + + + + + + + + + + From Location Icon + + + + + + + + + + + + + + + + + + + + + + + To Location Icon + + + + + + + + + + + + + + + +
viewers > stop viewer should render with OTP transit index Plan a trip: - - - - - - - - | - - - - - - + + + + + + + + + + + + From Location Icon + + + + + + + + + + + + + + + + + + + + + + + To Location Icon + + + + + + + + + + + + + + + +
viewers > stop viewer should render with TriMet transit in Plan a trip: - - - - - - - - | - - - - - - + + + + + + + + + + + + From Location Icon + + + + + + + + + + + + + + + + + + + + + + + To Location Icon + + + + + + + + + + + + + + + +
viewers > stop viewer', () => { afterEach(restoreDateNowBehavior) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 4bedf264f..36a8543f2 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -8,13 +8,13 @@ Object { "debouncePlanTimeMs": 0, "homeTimezone": "America/Los_Angeles", "language": Object {}, - "operators": Array [], "realtimeEffectsDisplayThreshold": 120, "routingTypes": Array [], "stopViewer": Object { "numberOfDepartures": 3, "timeRange": 345600, }, + "transitOperators": Array [], }, "currentQuery": Object { "bikeSpeed": 3.58, diff --git a/__tests__/test-utils/fixtures/geocoding/arcgis/findAddressCandidates-response.json b/__tests__/test-utils/fixtures/geocoding/arcgis/findAddressCandidates-response.json deleted file mode 100644 index 9c63ea07f..000000000 --- a/__tests__/test-utils/fixtures/geocoding/arcgis/findAddressCandidates-response.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "spatialReference": { - "wkid": 4326, - "latestWkid": 4326 - }, - "candidates": [ - { - "address": "Mill End, Clavering, Saffron Walden, Essex, England", - "location": { - "x": 0.12961000000007061, - "y": 51.973520000000065 - }, - "score": 87.5, - "attributes": { - "Loc_name": "World", - "Status": "T", - "Score": 87.5, - "Match_addr": "Mill End, Clavering, Saffron Walden, Essex, England", - "LongLabel": "Mill End, Clavering, Saffron Walden, Essex, England, GBR", - "ShortLabel": "Mill End", - "Addr_type": "Locality", - "Type": "Village", - "PlaceName": "Mill End", - "Place_addr": "Clavering, Saffron Walden, Essex, England", - "Phone": "", - "URL": "", - "Rank": 15, - "AddBldg": "", - "AddNum": "", - "AddNumFrom": "", - "AddNumTo": "", - "AddRange": "", - "Side": "", - "StPreDir": "", - "StPreType": "", - "StName": "", - "StType": "", - "StDir": "", - "BldgType": "", - "BldgName": "", - "LevelType": "", - "LevelName": "", - "UnitType": "", - "UnitName": "", - "SubAddr": "", - "StAddr": "", - "Block": "", - "Sector": "", - "Nbrhd": "Mill End", - "District": "Clavering", - "City": "Saffron Walden", - "MetroArea": "", - "Subregion": "Essex", - "Region": "England", - "RegionAbbr": "ENG", - "Territory": "", - "Zone": "", - "Postal": "", - "PostalExt": "", - "Country": "GBR", - "LangCode": "ENG", - "Distance": 0, - "X": 0.12961000000007061, - "Y": 51.973520000000065, - "DisplayX": 0.12961000000007061, - "DisplayY": 51.973520000000065, - "Xmin": 0.11961000000007062, - "Xmax": 0.13961000000007062, - "Ymin": 51.963520000000067, - "Ymax": 51.983520000000063, - "ExInfo": "" - }, - "extent": { - "xmin": 0.11961000000007062, - "ymin": 51.963520000000067, - "xmax": 0.13961000000007062, - "ymax": 51.983520000000063 - } - }, - { - "address": "Mill End, Hambleden, Henley-on-Thames, Oxfordshire, England", - "location": { - "x": -0.86689999999993006, - "y": 51.558340000000044 - }, - "score": 87.5, - "attributes": { - "Loc_name": "World", - "Status": "T", - "Score": 87.5, - "Match_addr": "Mill End, Hambleden, Henley-on-Thames, Oxfordshire, England", - "LongLabel": "Mill End, Hambleden, Henley-on-Thames, Oxfordshire, England, GBR", - "ShortLabel": "Mill End", - "Addr_type": "Locality", - "Type": "Village", - "PlaceName": "Mill End", - "Place_addr": "Hambleden, Henley-on-Thames, Oxfordshire, England", - "Phone": "", - "URL": "", - "Rank": 15, - "AddBldg": "", - "AddNum": "", - "AddNumFrom": "", - "AddNumTo": "", - "AddRange": "", - "Side": "", - "StPreDir": "", - "StPreType": "", - "StName": "", - "StType": "", - "StDir": "", - "BldgType": "", - "BldgName": "", - "LevelType": "", - "LevelName": "", - "UnitType": "", - "UnitName": "", - "SubAddr": "", - "StAddr": "", - "Block": "", - "Sector": "", - "Nbrhd": "Mill End", - "District": "Hambleden", - "City": "Henley-on-Thames", - "MetroArea": "", - "Subregion": "Oxfordshire", - "Region": "England", - "RegionAbbr": "ENG", - "Territory": "", - "Zone": "", - "Postal": "", - "PostalExt": "", - "Country": "GBR", - "LangCode": "ENG", - "Distance": 0, - "X": -0.86689999999993006, - "Y": 51.558340000000044, - "DisplayX": -0.86689999999993006, - "DisplayY": 51.558340000000044, - "Xmin": -0.87689999999993007, - "Xmax": -0.85689999999993005, - "Ymin": 51.548340000000046, - "Ymax": 51.568340000000042, - "ExInfo": "" - }, - "extent": { - "xmin": -0.87689999999993007, - "ymin": 51.548340000000046, - "xmax": -0.85689999999993005, - "ymax": 51.568340000000042 - } - } - ] -} diff --git a/__tests__/test-utils/fixtures/geocoding/arcgis/reverseGeocode-response.json b/__tests__/test-utils/fixtures/geocoding/arcgis/reverseGeocode-response.json deleted file mode 100644 index 2b73a1e4c..000000000 --- a/__tests__/test-utils/fixtures/geocoding/arcgis/reverseGeocode-response.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "address": { - "Match_addr": "SW Naito Pkwy, Portland, Oregon, 97204", - "LongLabel": "SW Naito Pkwy, Portland, OR, 97204, USA", - "ShortLabel": "SW Naito Pkwy", - "Addr_type": "StreetName", - "Type": "", - "PlaceName": "", - "AddNum": "", - "Address": "SW Naito Pkwy", - "Block": "", - "Sector": "", - "Neighborhood": "Downtown", - "District": "", - "City": "Portland", - "MetroArea": "Portland-Vancouver Metro Area", - "Subregion": "Multnomah County", - "Region": "Oregon", - "Territory": "", - "Postal": "97204", - "PostalExt": "", - "CountryCode": "USA" - }, - "location": { - "x": -122.67320084756255, - "y": 45.516175523600715, - "spatialReference": { - "wkid": 4326, - "latestWkid": 4326 - } - } -} diff --git a/__tests__/test-utils/fixtures/geocoding/arcgis/suggest-response.json b/__tests__/test-utils/fixtures/geocoding/arcgis/suggest-response.json deleted file mode 100644 index 61c72abaf..000000000 --- a/__tests__/test-utils/fixtures/geocoding/arcgis/suggest-response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "suggestions": [ - { - "text": "Mill Ends City Park, Portland, OR, USA", - "magicKey": "dHA9MCNsb2M9ODU3OTE0NyNsbmc9MzMjcGw9MzI4OTA3MSNsYnM9MTQ6MjAwNTI3NTc=", - "isCollection": false - }, - { - "text": "Mill End Sports & Social, Penn Road, Mill End, Rickmansworth, Hertfordshire, England, WD3 8, GBR", - "magicKey": "dHA9MCNsb2M9OTY5Mzc0MiNsbmc9MzMjcGw9NTA4NTQ2NiNsYnM9MTQ6MjAwNTI3NTI=", - "isCollection": false - } - ] -} diff --git a/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json b/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json deleted file mode 100644 index ea9758c84..000000000 --- a/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "geocoding": { - "version": "0.2", - "attribution": "http://lb-st-mapgeo.tri-met.org:4000/attribution", - "query": { - "text": "Mill Ends", - "parser": "addressit", - "parsed_text": {}, - "tokens": [ - "Mill", - "Ends" - ], - "size": 10, - "private": false, - "focus.point.lat": 45.52, - "focus.point.lon": -122.67, - "lang": { - "name": "English", - "iso6391": "en", - "iso6393": "eng", - "defaulted": true - } - }, - "warnings": [ - "Invalid Parameter: api_key" - ], - "engine": { - "name": "Pelias", - "author": "Mapzen", - "version": "1.0" - }, - "timestamp": 1563948844191 - }, - "type": "FeatureCollection", - "features": [{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -122.673377, - 45.516278 - ] - }, - "properties": { - "id": "node/4243944023", - "gid": "openstreetmap:venue:node/4243944023", - "layer": "venue", - "source": "openstreetmap", - "source_id": "node/4243944023", - "name": "Mill Ends Park", - "distance": 0.491, - "accuracy": "point", - "country": "United States", - "country_gid": "whosonfirst:country:85633793", - "country_a": "USA", - "region": "Oregon", - "region_gid": "whosonfirst:region:85688513", - "region_a": "OR", - "county": "Multnomah County", - "county_gid": "whosonfirst:county:102081631", - "county_a": "MU", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "label": "Mill Ends Park, Portland, OR, USA" - } - }], - "bbox": [ - -122.673377, - 45.516278, - -122.673377, - 45.516278 - ] -} diff --git a/__tests__/test-utils/fixtures/geocoding/pelias/reverse-response.json b/__tests__/test-utils/fixtures/geocoding/pelias/reverse-response.json deleted file mode 100644 index 0516458c4..000000000 --- a/__tests__/test-utils/fixtures/geocoding/pelias/reverse-response.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "geocoding": { - "version": "0.2", - "attribution": "http://lb-st-mapgeo.tri-met.org:4000/attribution", - "query": { - "size": 10, - "private": false, - "point.lat": 45.516198, - "point.lon": -122.67324, - "boundary.circle.lat": 45.516198, - "boundary.circle.lon": -122.67324, - "lang": { - "name": "English", - "iso6391": "en", - "iso6393": "eng", - "defaulted": true - }, - "querySize": 20 - }, - "warnings": [ - "Invalid Parameter: api_key" - ], - "engine": { - "name": "Pelias", - "author": "Mapzen", - "version": "1.0" - }, - "timestamp": 1563949092620 - }, - "type": "FeatureCollection", - "features": [{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -122.673245, - 45.516201 - ] - }, - "properties": { - "id": "way/156366633", - "gid": "openstreetmap:venue:way/156366633", - "layer": "venue", - "source": "openstreetmap", - "source_id": "way/156366633", - "name": "Mill Ends Park", - "confidence": 0.9, - "distance": 0.001, - "accuracy": "point", - "country": "United States", - "country_gid": "whosonfirst:country:85633793", - "country_a": "USA", - "region": "Oregon", - "region_gid": "whosonfirst:region:85688513", - "region_a": "OR", - "county": "Multnomah County", - "county_gid": "whosonfirst:county:102081631", - "county_a": "MU", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "label": "Mill Ends Park, Portland, OR, USA" - }, - "bbox": [ - -122.6732557, - 45.5161964, - -122.673234, - 45.5162067 - ] - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -122.673609, - 45.516337 - ] - }, - "properties": { - "id": "us/or/portland_metro:bd3f9863242e4ec7", - "gid": "openaddresses:address:us/or/portland_metro:bd3f9863242e4ec7", - "layer": "address", - "source": "openaddresses", - "source_id": "us/or/portland_metro:bd3f9863242e4ec7", - "name": "833 SW Naito Pkwy", - "housenumber": "833", - "street": "SW Naito Pkwy", - "postalcode": "97204", - "confidence": 0.8, - "distance": 0.033, - "accuracy": "point", - "country": "United States", - "country_gid": "whosonfirst:country:85633793", - "country_a": "USA", - "region": "Oregon", - "region_gid": "whosonfirst:region:85688513", - "region_a": "OR", - "county": "Multnomah County", - "county_gid": "whosonfirst:county:102081631", - "county_a": "MU", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "label": "833 SW Naito Pkwy, Portland, OR, USA" - } - } - ], - "bbox": [ - -122.673883, - 45.5161964, - -122.673234, - 45.516689 - ] -} diff --git a/__tests__/test-utils/fixtures/geocoding/pelias/search-response.json b/__tests__/test-utils/fixtures/geocoding/pelias/search-response.json deleted file mode 100644 index 61e6407a6..000000000 --- a/__tests__/test-utils/fixtures/geocoding/pelias/search-response.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "geocoding": { - "version": "0.2", - "attribution": "http://lb-st-mapgeo.tri-met.org:4000/attribution", - "query": { - "text": "Mill Ends", - "size": 10, - "private": false, - "focus.point.lat": 45.52, - "focus.point.lon": -122.67, - "lang": { - "name": "English", - "iso6391": "en", - "iso6393": "eng", - "defaulted": true - }, - "querySize": 20, - "parser": "addressit", - "parsed_text": {} - }, - "warnings": [ - "Invalid Parameter: api_key" - ], - "engine": { - "name": "Pelias", - "author": "Mapzen", - "version": "1.0" - }, - "timestamp": 1563949031708 - }, - "type": "FeatureCollection", - "features": [{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -122.673377, - 45.516278 - ] - }, - "properties": { - "id": "node/4243944023", - "gid": "openstreetmap:venue:node/4243944023", - "layer": "venue", - "source": "openstreetmap", - "source_id": "node/4243944023", - "name": "Mill Ends Park", - "confidence": 1, - "match_type": "exact", - "distance": 0.491, - "accuracy": "point", - "country": "United States", - "country_gid": "whosonfirst:country:85633793", - "country_a": "USA", - "region": "Oregon", - "region_gid": "whosonfirst:region:85688513", - "region_a": "OR", - "county": "Multnomah County", - "county_gid": "whosonfirst:county:102081631", - "county_a": "MU", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "label": "Mill Ends Park, Portland, OR, USA" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -122.681441, - 45.511851 - ] - }, - "properties": { - "id": "2584-1136::TRIMET::station", - "gid": "transit:station:2584-1136::TRIMET::station", - "layer": "station", - "source": "transit", - "source_id": "2584-1136::trimet::station", - "name": "PSU Urban Center/SW 5th & Mill MAX Station", - "street": "SW 5th & Mill", - "postalcode": "97201", - "confidence": 1, - "match_type": "exact", - "distance": 1.273, - "accuracy": "centroid", - "country": "United States", - "country_gid": "whosonfirst:country:85633793", - "country_a": "USA", - "region": "Oregon", - "region_gid": "whosonfirst:region:85688513", - "region_a": "OR", - "county": "Multnomah County", - "county_gid": "whosonfirst:county:102081631", - "county_a": "MU", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "label": "PSU Urban Center/SW 5th & Mill MAX Station, Portland, OR, USA" - } - } - ], - "bbox": [ - -122.821522, - 45.495264, - -122.475528, - 45.516278 - ] -} diff --git a/__tests__/util/__mocks__/itinerary.json b/__tests__/util/__mocks__/itinerary.json deleted file mode 100644 index c00a605a4..000000000 --- a/__tests__/util/__mocks__/itinerary.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "route1": { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10 - }, - "route2": { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2 - }, - "route3": { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3 - }, - "route4": { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999 - }, - "route5": { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999 - }, - "route6": { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999 - }, - "route7": { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999 - }, - "route8": { - "longName": "Zig-zagging route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route9": { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route10": { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2 - }, - "route11": { - "longName": "Local route", - "mode": "BUS", - "shortName": "6", - "sortOrder": 2 - }, - "route12": { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2 - }, - "route13": { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2 - }, - "route14": { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2 - }, - "route15": { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2 - } -} diff --git a/__tests__/util/__snapshots__/geocoder.js.snap b/__tests__/util/__snapshots__/geocoder.js.snap deleted file mode 100644 index 0ec26af09..000000000 --- a/__tests__/util/__snapshots__/geocoder.js.snap +++ /dev/null @@ -1,350 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`geocoder ARCGIS should get location from geocode feature 1`] = ` -Object { - "lat": 51.973520000000065, - "lon": 0.1296100000000706, - "name": "Mill End, Clavering, Saffron Walden, Essex, England, GBR", -} -`; - -exports[`geocoder ARCGIS should make autocomplete query 1`] = ` -Object { - "features": Array [ - Object { - "isCollection": false, - "magicKey": "dHA9MCNsb2M9ODU3OTE0NyNsbmc9MzMjcGw9MzI4OTA3MSNsYnM9MTQ6MjAwNTI3NTc=", - "properties": Object { - "label": "Mill Ends City Park, Portland, OR, USA", - }, - "text": "Mill Ends City Park, Portland, OR, USA", - }, - Object { - "isCollection": false, - "magicKey": "dHA9MCNsb2M9OTY5Mzc0MiNsbmc9MzMjcGw9NTA4NTQ2NiNsYnM9MTQ6MjAwNTI3NTI=", - "properties": Object { - "label": "Mill End Sports & Social, Penn Road, Mill End, Rickmansworth, Hertfordshire, England, WD3 8, GBR", - }, - "text": "Mill End Sports & Social, Penn Road, Mill End, Rickmansworth, Hertfordshire, England, WD3 8, GBR", - }, - ], -} -`; - -exports[`geocoder ARCGIS should make reverse query 1`] = ` -Object { - "lat": 45.516198, - "lon": -122.67324, - "name": "SW Naito Pkwy, Portland, OR, 97204, USA", -} -`; - -exports[`geocoder ARCGIS should make search query 1`] = ` -Object { - "features": Array [ - Object { - "geometry": Object { - "coordinates": Array [ - 0.1296100000000706, - 51.973520000000065, - ], - "type": "point", - }, - "properties": Object { - "confidence": 0.875, - "country": "GBR", - "country_a": "GBR", - "county": "Essex", - "label": "Mill End, Clavering, Saffron Walden, Essex, England, GBR", - "locality": "Saffron Walden", - "name": "Mill End", - "neighbourhood": "Mill End", - "region": "England", - "resultId": undefined, - }, - "type": "feature", - }, - Object { - "geometry": Object { - "coordinates": Array [ - -0.8668999999999301, - 51.558340000000044, - ], - "type": "point", - }, - "properties": Object { - "confidence": 0.875, - "country": "GBR", - "country_a": "GBR", - "county": "Oxfordshire", - "label": "Mill End, Hambleden, Henley-on-Thames, Oxfordshire, England, GBR", - "locality": "Henley-on-Thames", - "name": "Mill End", - "neighbourhood": "Mill End", - "region": "England", - "resultId": undefined, - }, - "type": "feature", - }, - ], - "query": Object { - "text": "Mill Ends", - }, -} -`; - -exports[`geocoder NoApiGeocoder should get location from geocode feature 1`] = ` -Object { - "lat": 45.516198, - "lon": -122.67324, - "name": "45.516198, -122.673240", -} -`; - -exports[`geocoder NoApiGeocoder should make autocomplete query 1`] = ` -Object { - "features": Array [], -} -`; - -exports[`geocoder NoApiGeocoder should make reverse query 1`] = ` -Object { - "lat": 45.5162, - "lon": -122.67324, - "name": "45.5162, -122.67324", -} -`; - -exports[`geocoder NoApiGeocoder should make search query 1`] = ` -Object { - "features": Array [], -} -`; - -exports[`geocoder PELIAS should get location from geocode feature 1`] = ` -Object { - "lat": 45.516198, - "lon": -122.67324, - "name": "Mill Ends Park, Portland, OR, USA", -} -`; - -exports[`geocoder PELIAS should make autocomplete query 1`] = ` -Object { - "bbox": Array [ - -122.673377, - 45.516278, - -122.673377, - 45.516278, - ], - "features": Array [ - Object { - "geometry": Object { - "coordinates": Array [ - -122.673377, - 45.516278, - ], - "type": "Point", - }, - "properties": Object { - "accuracy": "point", - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "country": "United States", - "country_a": "USA", - "country_gid": "whosonfirst:country:85633793", - "county": "Multnomah County", - "county_a": "MU", - "county_gid": "whosonfirst:county:102081631", - "distance": 0.491, - "gid": "openstreetmap:venue:node/4243944023", - "id": "node/4243944023", - "label": "Mill Ends Park, Portland, OR, USA", - "layer": "venue", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "name": "Mill Ends Park", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "region": "Oregon", - "region_a": "OR", - "region_gid": "whosonfirst:region:85688513", - "source": "openstreetmap", - "source_id": "node/4243944023", - }, - "type": "Feature", - }, - ], - "geocoding": Object { - "attribution": "http://lb-st-mapgeo.tri-met.org:4000/attribution", - "engine": Object { - "author": "Mapzen", - "name": "Pelias", - "version": "1.0", - }, - "query": Object { - "focus.point.lat": 45.52, - "focus.point.lon": -122.67, - "lang": Object { - "defaulted": true, - "iso6391": "en", - "iso6393": "eng", - "name": "English", - }, - "parsed_text": Object {}, - "parser": "addressit", - "private": false, - "size": 10, - "text": "Mill Ends", - "tokens": Array [ - "Mill", - "Ends", - ], - }, - "timestamp": 1563948844191, - "version": "0.2", - "warnings": Array [ - "Invalid Parameter: api_key", - ], - }, - "isomorphicMapzenSearchQuery": Object { - "api_key": "dummy-mapzen-key", - "text": "Mill Ends", - }, - "type": "FeatureCollection", -} -`; - -exports[`geocoder PELIAS should make reverse query 1`] = ` -Object { - "lat": 45.516198, - "lon": -122.67324, - "name": "Mill Ends Park, Portland, OR, USA", -} -`; - -exports[`geocoder PELIAS should make search query 1`] = ` -Object { - "bbox": Array [ - -122.821522, - 45.495264, - -122.475528, - 45.516278, - ], - "features": Array [ - Object { - "geometry": Object { - "coordinates": Array [ - -122.673377, - 45.516278, - ], - "type": "Point", - }, - "properties": Object { - "accuracy": "point", - "confidence": 1, - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "country": "United States", - "country_a": "USA", - "country_gid": "whosonfirst:country:85633793", - "county": "Multnomah County", - "county_a": "MU", - "county_gid": "whosonfirst:county:102081631", - "distance": 0.491, - "gid": "openstreetmap:venue:node/4243944023", - "id": "node/4243944023", - "label": "Mill Ends Park, Portland, OR, USA", - "layer": "venue", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "match_type": "exact", - "name": "Mill Ends Park", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "region": "Oregon", - "region_a": "OR", - "region_gid": "whosonfirst:region:85688513", - "source": "openstreetmap", - "source_id": "node/4243944023", - }, - "type": "Feature", - }, - Object { - "geometry": Object { - "coordinates": Array [ - -122.681441, - 45.511851, - ], - "type": "Point", - }, - "properties": Object { - "accuracy": "centroid", - "confidence": 1, - "continent": "North America", - "continent_gid": "whosonfirst:continent:102191575", - "country": "United States", - "country_a": "USA", - "country_gid": "whosonfirst:country:85633793", - "county": "Multnomah County", - "county_a": "MU", - "county_gid": "whosonfirst:county:102081631", - "distance": 1.273, - "gid": "transit:station:2584-1136::TRIMET::station", - "id": "2584-1136::TRIMET::station", - "label": "PSU Urban Center/SW 5th & Mill MAX Station, Portland, OR, USA", - "layer": "station", - "locality": "Portland", - "locality_gid": "whosonfirst:locality:101715829", - "match_type": "exact", - "name": "PSU Urban Center/SW 5th & Mill MAX Station", - "neighbourhood": "Downtown", - "neighbourhood_gid": "whosonfirst:neighbourhood:85867131", - "postalcode": "97201", - "region": "Oregon", - "region_a": "OR", - "region_gid": "whosonfirst:region:85688513", - "source": "transit", - "source_id": "2584-1136::trimet::station", - "street": "SW 5th & Mill", - }, - "type": "Feature", - }, - ], - "geocoding": Object { - "attribution": "http://lb-st-mapgeo.tri-met.org:4000/attribution", - "engine": Object { - "author": "Mapzen", - "name": "Pelias", - "version": "1.0", - }, - "query": Object { - "focus.point.lat": 45.52, - "focus.point.lon": -122.67, - "lang": Object { - "defaulted": true, - "iso6391": "en", - "iso6393": "eng", - "name": "English", - }, - "parsed_text": Object {}, - "parser": "addressit", - "private": false, - "querySize": 20, - "size": 10, - "text": "Mill Ends", - }, - "timestamp": 1563949031708, - "version": "0.2", - "warnings": Array [ - "Invalid Parameter: api_key", - ], - }, - "isomorphicMapzenSearchQuery": Object { - "api_key": "dummy-mapzen-key", - "size": 10, - "text": "Mill Ends", - }, - "type": "FeatureCollection", -} -`; diff --git a/__tests__/util/__snapshots__/itinerary.js.snap b/__tests__/util/__snapshots__/itinerary.js.snap deleted file mode 100644 index 6887d0868..000000000 --- a/__tests__/util/__snapshots__/itinerary.js.snap +++ /dev/null @@ -1,247 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`util > itinerary routeComparator should prioritize routes with integer shortNames over alphabetic shortNames 1`] = ` -Array [ - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should prioritize routes with shortNames over those with just longNames 1`] = ` -Array [ - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, - Object { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should prioritize routes with valid sortOrder 1`] = ` -Array [ - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, -] -`; - -exports[`util > itinerary routeComparator should sort based off of route type 1`] = ` -Array [ - Object { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2, - }, - Object { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of integer shortName 1`] = ` -Array [ - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of longNames 1`] = ` -Array [ - Object { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of shortNames 1`] = ` -Array [ - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes based off of sortOrder 1`] = ` -Array [ - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes on all of the criteria at once 1`] = ` -Array [ - Object { - "longName": "Yellow line Subway", - "mode": "SUBWAY", - "shortName": "Yellow", - "sortOrder": 2, - }, - Object { - "longName": "Intercity Train", - "mode": "RAIL", - "shortName": "IC", - "sortOrder": 2, - }, - Object { - "longName": "Local route", - "mode": "BUS", - "shortName": "6", - "sortOrder": 2, - }, - Object { - "longName": "Around town", - "mode": "BUS", - "shortName": "20", - "sortOrder": 2, - }, - Object { - "longName": "Express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Variation of express route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Zig-zagging route", - "mode": "BUS", - "shortName": "30", - "sortOrder": 2, - }, - Object { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2, - }, - Object { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2, - }, - Object { - "longName": "Across town", - "mode": "BUS", - "shortName": "10", - "sortOrder": 10, - }, - Object { - "longName": "A-line", - "mode": "BUS", - "shortName": "A", - "sortOrder": -999, - }, - Object { - "longName": "B-line", - "mode": "BUS", - "shortName": "B", - "sortOrder": -999, - }, - Object { - "longName": "Loop route", - "mode": "BUS", - "shortName": "2", - "sortOrder": -999, - }, - Object { - "longName": "Around another town", - "shortName": "3", - "sortOrder": -999, - "type": 3, - }, - Object { - "longName": "A meandering route", - "mode": "BUS", - "sortOrder": -999, - }, -] -`; - -exports[`util > itinerary routeComparator should sort routes with alphanumeric shortNames 1`] = ` -Array [ - Object { - "longName": "Xpress route C", - "mode": "BUS", - "shortName": "30C", - "sortOrder": 2, - }, - Object { - "longName": "Express route X", - "mode": "BUS", - "shortName": "30X", - "sortOrder": 2, - }, -] -`; diff --git a/__tests__/util/geocoder.js b/__tests__/util/geocoder.js deleted file mode 100644 index ada6e8601..000000000 --- a/__tests__/util/geocoder.js +++ /dev/null @@ -1,188 +0,0 @@ -import nock from 'nock' - -import getGeocoder, { PeliasGeocoder } from '../../lib/util/geocoder' - -function mockResponsePath (geocoder, file) { - return `__tests__/test-utils/fixtures/geocoding/${geocoder}/${file}` -} - -describe('geocoder', () => { - const geocoders = [ - { - type: 'ARCGIS' - }, { - apiKey: 'dummy-mapzen-key', - baseUrl: 'https://ws-st.trimet.org/pelias/v1', - type: 'PELIAS' - }, - // this entry represents no geocoder configuration. In this case it is - // expected that the NoApiGeocoder will be used. - undefined - ] - - // nocks for ARCGIS - const baseArcGisPath = '/arcgis/rest/services/World/GeocodeServer/' - nock('https://geocode.arcgis.com') - // autocomplete - .get(`${baseArcGisPath}suggest`) - .query(true) - .replyWithFile(200, mockResponsePath('arcgis', 'suggest-response.json')) - // reverse - .get(`${baseArcGisPath}reverseGeocode`) - .query(true) - .replyWithFile(200, mockResponsePath('arcgis', 'reverseGeocode-response.json')) - // search - .get(`${baseArcGisPath}findAddressCandidates`) - .query(true) - .replyWithFile(200, mockResponsePath('arcgis', 'findAddressCandidates-response.json')) - // a 2nd search for purposes of resolving getLocationFromGeocodedFeature test - .get(`${baseArcGisPath}findAddressCandidates`) - .query(true) - .replyWithFile(200, mockResponsePath('arcgis', 'findAddressCandidates-response.json')) - - // nocks for PELIAS - const basePeliasPath = '/pelias/v1/' - nock('https://ws-st.trimet.org') - // autocomplete - .get(`${basePeliasPath}autocomplete`) - .query(true) - .replyWithFile(200, mockResponsePath('pelias', 'autocomplete-response.json')) - // reverse - .get(`${basePeliasPath}search`) - .query(true) - .replyWithFile(200, mockResponsePath('pelias', 'search-response.json')) - // search - .get(`${basePeliasPath}reverse`) - .query(true) - .replyWithFile(200, mockResponsePath('pelias', 'reverse-response.json')) - - geocoders.forEach(geocoder => { - const geocoderType = geocoder - ? geocoder.type - : 'NoApiGeocoder' - // the describe is in quotes to bypass a lint rule - describe(`${geocoderType}`, () => { - it('should make autocomplete query', async () => { - const result = await getGeocoder(geocoder).autocomplete({ text: 'Mill Ends' }) - expect(result).toMatchSnapshot() - }) - - it('should make search query', async () => { - const result = await getGeocoder(geocoder).search({ text: 'Mill Ends' }) - expect(result).toMatchSnapshot() - }) - - it('should make reverse query', async () => { - const result = await getGeocoder(geocoder) - .reverse({ point: { lat: 45.516198, lon: -122.673240 } }) - expect(result).toMatchSnapshot() - }) - - it('should get location from geocode feature', async () => { - let mockFeature - switch (geocoderType) { - case 'ARCGIS': - mockFeature = { - magicKey: 'abcd', - properties: { - label: 'Mill Ends City Park, Portland, OR, USA' - }, - text: 'Mill Ends City Park, Portland, OR, USA' - } - break - case 'PELIAS': - mockFeature = { - geometry: { - coordinates: [-122.673240, 45.516198], - type: 'Point' - }, - properties: { - label: 'Mill Ends Park, Portland, OR, USA' - } - } - break - case 'NoApiGeocoder': - mockFeature = { - geometry: { - coordinates: [-122.673240, 45.516198], - type: 'Point' - }, - properties: { - label: '45.516198, -122.673240' - } - } - break - default: - throw new Error(`no mock feature defined for geocoder type: ${geocoder.type}`) - } - const result = await getGeocoder(geocoder).getLocationFromGeocodedFeature(mockFeature) - expect(result).toMatchSnapshot() - }) - - // geocoder-specific tests - if (geocoderType === 'PELIAS') { - const mockSources = 'gn,oa,osm,wof' - - // sources should not be sent unless they are explicitly defined in the - // query. See https://github.com/ibi-group/trimet-mod-otp/issues/239 - it('should not send sources in autocomplete by default', () => { - // create mock API to check query - const mockPeliasAPI = { - autocomplete: query => { - expect(query.sources).not.toBe(expect.anything()) - return Promise.resolve() - } - } - const pelias = new PeliasGeocoder(mockPeliasAPI, geocoder) - pelias.autocomplete({ text: 'Mill Ends' }) - }) - - // should send sources if they're defined in the config - it('should send sources in autocomplete if defined in config', () => { - // create mock API to check query - const mockPeliasAPI = { - autocomplete: query => { - expect(query.sources).toBe(mockSources) - return Promise.resolve() - } - } - const pelias = new PeliasGeocoder( - mockPeliasAPI, - { ...geocoder, sources: mockSources } - ) - pelias.autocomplete({ text: 'Mill Ends' }) - }) - - // sources should not be sent unless they are explicitly defined in the - // query. See https://github.com/ibi-group/trimet-mod-otp/issues/239 - it('should not send sources in search by default', () => { - // create mock API to check query - const mockPeliasAPI = { - search: query => { - expect(query.sources).not.toBe(expect.anything()) - return Promise.resolve() - } - } - const pelias = new PeliasGeocoder(mockPeliasAPI, geocoder) - pelias.search({ text: 'Mill Ends' }) - }) - - // should send sources if they're defined in the config - it('should send sources in search if defined in config', () => { - // create mock API to check query - const mockPeliasAPI = { - search: query => { - expect(query.sources).toBe(mockSources) - return Promise.resolve() - } - } - const pelias = new PeliasGeocoder( - mockPeliasAPI, - { ...geocoder, sources: mockSources } - ) - pelias.search({ text: 'Mill Ends' }) - }) - } - }) - }) -}) diff --git a/__tests__/util/itinerary.js b/__tests__/util/itinerary.js deleted file mode 100644 index 74e5d5a56..000000000 --- a/__tests__/util/itinerary.js +++ /dev/null @@ -1,88 +0,0 @@ -import {isTransit, routeComparator} from '../../lib/util/itinerary' - -const { - route1, - route2, - route3, - route4, - route5, - route6, - route7, - route8, - route9, - route10, - route11, - route12, - route13, - route14, - route15 -} = require('./__mocks__/itinerary.json') - -function sortRoutes (...routes) { - routes.sort(routeComparator) - return routes -} - -describe('util > itinerary', () => { - it('isTransit should work', () => { - expect(isTransit('CAR')).toBeFalsy() - }) - - describe('routeComparator', () => { - it('should sort routes based off of sortOrder', () => { - expect(sortRoutes(route1, route2)).toMatchSnapshot() - }) - - it('should prioritize routes with valid sortOrder', () => { - expect(sortRoutes(route2, route3)).toMatchSnapshot() - }) - - it('should sort routes based off of integer shortName', () => { - expect(sortRoutes(route3, route4)).toMatchSnapshot() - }) - - it('should prioritize routes with integer shortNames over alphabetic shortNames', () => { - expect(sortRoutes(route4, route5)).toMatchSnapshot() - }) - - it('should sort routes based off of shortNames', () => { - expect(sortRoutes(route5, route6)).toMatchSnapshot() - }) - - it('should sort routes with alphanumeric shortNames', () => { - expect(sortRoutes(route14, route15)).toMatchSnapshot() - }) - - it('should prioritize routes with shortNames over those with just longNames', () => { - expect(sortRoutes(route6, route7)).toMatchSnapshot() - }) - - it('should sort routes based off of longNames', () => { - expect(sortRoutes(route9, route10)).toMatchSnapshot() - }) - - it('should sort routes on all of the criteria at once', () => { - expect(sortRoutes( - route1, - route2, - route3, - route4, - route5, - route6, - route7, - route8, - route9, - route10, - route11, - route12, - route13, - route14, - route15 - )).toMatchSnapshot() - }) - - it('should sort based off of route type', () => { - expect(sortRoutes(route12, route13)).toMatchSnapshot() - }) - }) -}) diff --git a/custom-icons.js b/custom-icons.js new file mode 100644 index 000000000..dbd806f10 --- /dev/null +++ b/custom-icons.js @@ -0,0 +1,47 @@ +import { + ClassicBus, + ClassicGondola, + ClassicModeIcon, + Ferry, + LegIcon, + StandardGondola +} from '@opentripplanner/icons' + +/** + * For more advanced users, you can replicate and customize components and + * observe the change in icons. + * - For LegIcon: https://github.com/opentripplanner/otp-ui/blob/master/packages/icons/src/trimet-leg-icon.js + * - For ModeIcon: https://github.com/opentripplanner/otp-ui/blob/master/packages/icons/src/trimet-mode-icon.js + * The example below shuffles some icons around from what you might normally + * expect for demonstration purposes. + */ + +const CustomTransitIcon = Ferry +const CustomRailIcon = ClassicGondola +const CustomStreetcarIcon = StandardGondola +const CustomBikeRentalIcon = ClassicBus + +export const CustomModeIcon = ({ mode, ...props }) => { + if (!mode) return null + switch (mode.toLowerCase()) { + // Place custom icons for each mode here. + case 'transit': + return + case 'rail': + return + default: + return + } +} + +export const CustomLegIcon = ({ leg, ...props }) => { + if ( + leg.routeLongName && + leg.routeLongName.startsWith('MAX') + ) { + return + } else if (leg.rentedBike) { + return + } + return +} diff --git a/example.js b/example.js index b29df8943..3d6530aa6 100644 --- a/example.js +++ b/example.js @@ -1,6 +1,7 @@ // import this polyfill in order to make webapp compatible with IE 11 import 'es6-math' +import {ClassicLegIcon, ClassicModeIcon} from '@opentripplanner/icons' import { createHashHistory } from 'history' import { connectRouter, routerMiddleware } from 'connected-react-router' import React, { Component } from 'react' @@ -9,26 +10,35 @@ import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' import thunk from 'redux-thunk' import createLogger from 'redux-logger' - // import Bootstrap Grid components for layout import { Navbar, Grid, Row, Col } from 'react-bootstrap' // import OTP-RR components import { - DefaultSearchForm, - ErrorMessage, + DefaultMainPanel, MobileMain, - NarrativeRoutingResults, ResponsiveWebapp, Map, - ViewerContainer, AppMenu, createOtpReducer } from './lib' - // load the OTP configuration import otpConfig from './config.yml' +// Set useCustomIcons to true to override classic icons with the exports from +// custom-icons.js +const useCustomIcons = false + +// Define icon sets for modes. +let MyLegIcon = ClassicLegIcon +let MyModeIcon = ClassicModeIcon + +if (useCustomIcons) { + const CustomIcons = require('./custom-icons') + MyLegIcon = CustomIcons.CustomLegIcon + MyModeIcon = CustomIcons.CustomModeIcon +} + // create an initial query for demo/testing purposes const initialQuery = { from: { @@ -81,15 +91,8 @@ class OtpRRExample extends Component { - - - -
- -
-
+ - @@ -100,7 +103,12 @@ class OtpRRExample extends Component { /** mobile view **/ const mobileView = ( - )} title={(
OpenTripPlanner
)} /> + } + title={
OpenTripPlanner
} + /> ) /** the main webapp **/ @@ -108,6 +116,7 @@ class OtpRRExample extends Component { ) } diff --git a/lib/actions/api.js b/lib/actions/api.js index 705822ecb..988e9900e 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1,20 +1,22 @@ /* globals fetch */ import { push, replace } from 'connected-react-router' +import haversine from 'haversine' +import moment from 'moment' import hash from 'object-hash' +import coreUtils from '@opentripplanner/core-utils' +import queryParams from '@opentripplanner/core-utils/lib/query-params' import { createAction } from 'redux-actions' import qs from 'qs' -import moment from 'moment' -import haversine from 'haversine' import { rememberPlace } from './map' -import { hasCar } from '../util/itinerary' -import { getTripOptionsFromQuery, getUrlParams } from '../util/query' -import queryParams from '../util/query-params' import { getStopViewerConfig, queryIsValid } from '../util/state' -import { randId } from '../util/storage' -import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '../util/time' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') +const { hasCar } = coreUtils.itinerary +const { getTripOptionsFromQuery, getUrlParams } = coreUtils.query +const { randId } = coreUtils.storage +const { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } = coreUtils.time + // Generic API actions export const nonRealtimeRoutingResponse = createAction('NON_REALTIME_ROUTING_RESPONSE') diff --git a/lib/actions/form.js b/lib/actions/form.js index 66860f1a4..abaa4ff00 100644 --- a/lib/actions/form.js +++ b/lib/actions/form.js @@ -1,18 +1,10 @@ import debounce from 'lodash.debounce' +import isEqual from 'lodash.isequal' import moment from 'moment' +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' -import isEqual from 'lodash.isequal' -import { - getDefaultQuery, - getTripOptionsFromQuery, - getUrlParams, - planParamsToQuery -} from '../util/query' -import { getItem, randId } from '../util/storage' import { queryIsValid } from '../util/state' -import { OTP_API_TIME_FORMAT } from '../util/time' -import { isMobile } from '../util/ui' import { MobileScreens, setMainPanelContent, @@ -21,6 +13,13 @@ import { import { routingQuery } from './api' +const { + getDefaultQuery, + getTripOptionsFromQuery, + getUrlParams, + planParamsToQuery +} = coreUtils.query + export const settingQueryParam = createAction('SET_QUERY_PARAM') export const clearActiveSearch = createAction('CLEAR_ACTIVE_SEARCH') export const setActiveSearch = createAction('SET_ACTIVE_SEARCH') @@ -35,7 +34,7 @@ export function resetForm () { dispatch(settingQueryParam(otpState.user.defaults)) } else { // Get user overrides and apply to default query - const userOverrides = getItem('defaultQuery', {}) + const userOverrides = coreUtils.storage.getItem('defaultQuery', {}) const defaultQuery = Object.assign( getDefaultQuery(otpState.config), userOverrides @@ -69,7 +68,7 @@ export function parseUrlQueryString (params = getUrlParams()) { Object.keys(params).forEach(key => { if (!key.startsWith('ui_')) planParams[key] = params[key] }) - const searchId = params.ui_activeSearch || randId() + const searchId = params.ui_activeSearch || coreUtils.storage.randId() // Convert strings to numbers/objects and dispatch dispatch( setQueryParam( @@ -89,10 +88,11 @@ let lastDebouncePlanTimeMs export function formChanged (oldQuery, newQuery) { return function (dispatch, getState) { const otpState = getState().otp + const isMobile = coreUtils.ui.isMobile() // If departArrive is set to 'NOW', update the query time to current if (otpState.currentQuery && otpState.currentQuery.departArrive === 'NOW') { - dispatch(settingQueryParam({ time: moment().format(OTP_API_TIME_FORMAT) })) + dispatch(settingQueryParam({ time: moment().format(coreUtils.time.OTP_API_TIME_FORMAT) })) } // Determine if either from/to location has changed @@ -111,7 +111,7 @@ export function formChanged (oldQuery, newQuery) { // either location changes only if not currently on welcome screen (otherwise // when the current position is auto-set the screen will change unexpectedly). if ( - isMobile() && + isMobile && (fromChanged || toChanged) && otpState.ui.mobileScreen !== MobileScreens.WELCOME_SCREEN ) { @@ -123,8 +123,8 @@ export function formChanged (oldQuery, newQuery) { const { autoPlan, debouncePlanTimeMs } = otpState.config const updatePlan = autoPlan || - (!isMobile() && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level? - (isMobile() && fromChanged && toChanged) + (!isMobile && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level? + (isMobile && fromChanged && toChanged) if (updatePlan && queryIsValid(otpState)) { // trip plan should be made // check if debouncing function needs to be (re)created if (!debouncedPlanTrip || lastDebouncePlanTimeMs !== debouncePlanTimeMs) { diff --git a/lib/actions/location.js b/lib/actions/location.js index 7adf4cbda..e4b778986 100644 --- a/lib/actions/location.js +++ b/lib/actions/location.js @@ -18,7 +18,7 @@ export function getCurrentPosition (setAsType = null, onSuccess) { dispatch(receivedPositionResponse({ position })) if (setAsType) { console.log('setting location to current position') - dispatch(setLocationToCurrent({ type: setAsType })) + dispatch(setLocationToCurrent({ locationType: setAsType })) onSuccess && onSuccess() } } else { diff --git a/lib/actions/map.js b/lib/actions/map.js index b1b0ece2e..f3ec18447 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -1,8 +1,9 @@ +import coreUtils from '@opentripplanner/core-utils' +import getGeocoder from '@opentripplanner/geocoder' import { createAction } from 'redux-actions' import { routingQuery } from './api' import { clearActiveSearch } from './form' -import getGeocoder from '../util/geocoder' /* SET_LOCATION action creator. Updates a from or to location in the store * @@ -35,6 +36,19 @@ export function clearLocation (payload) { } } +/** + * Handler for @opentripplanner/location-field onLocationSelected + */ +export function onLocationSelected ({ locationType, location, resultType }) { + return function (dispatch, getState) { + if (resultType === 'CURRENT_LOCATION') { + dispatch(setLocationToCurrent({ locationType })) + } else { + dispatch(setLocation({ location, locationType })) + } + } +} + export function setLocation (payload) { return function (dispatch, getState) { const otpState = getState().otp @@ -45,12 +59,12 @@ export function setLocation (payload) { .reverse({ point: payload.location }) .then((location) => { dispatch(settingLocation({ - type: payload.type, + locationType: payload.locationType, location })) }).catch(err => { dispatch(settingLocation({ - type: payload.type, + locationType: payload.locationType, location: payload.location })) console.warn(err) @@ -83,11 +97,11 @@ export function switchLocations () { const { from, to } = getState().otp.currentQuery // First, reverse the locations. dispatch(settingLocation({ - type: 'from', + locationType: 'from', location: to })) dispatch(settingLocation({ - type: 'to', + locationType: 'to', location: from })) // Then kick off a routing query (if the query is invalid, search will abort). @@ -101,11 +115,12 @@ export const setElevationPoint = createAction('SET_ELEVATION_POINT') export const setMapPopupLocation = createAction('SET_MAP_POPUP_LOCATION') -export function setMapPopupLocationAndGeocode (payload) { +export function setMapPopupLocationAndGeocode (mapEvent) { + const location = coreUtils.map.constructLocation(mapEvent.latlng) return function (dispatch, getState) { - dispatch(setMapPopupLocation(payload)) + dispatch(setMapPopupLocation({ location })) getGeocoder(getState().otp.config.geocoder) - .reverse({ point: payload.location }) + .reverse({ point: location }) .then((location) => { dispatch(setMapPopupLocation({ location })) }).catch(err => { diff --git a/lib/actions/narrative.js b/lib/actions/narrative.js index 8f016a400..56cc5279c 100644 --- a/lib/actions/narrative.js +++ b/lib/actions/narrative.js @@ -1,14 +1,14 @@ +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { setUrlSearch } from './api' -import { getUrlParams } from '../util/query' export function setActiveItinerary (payload) { return function (dispatch, getState) { // Trigger change in store. dispatch(settingActiveitinerary(payload)) // Update URL params. - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() urlParams.ui_activeItinerary = payload.index dispatch(setUrlSearch(urlParams)) } diff --git a/lib/actions/ui.js b/lib/actions/ui.js index ad04d2805..9dc8decfe 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -1,13 +1,14 @@ +import { push } from 'connected-react-router' +import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { matchPath } from 'react-router' -import { push } from 'connected-react-router' import { findRoute } from './api' import { setMapCenter, setMapZoom, setRouterId } from './config' import { clearActiveSearch, parseUrlQueryString, setActiveSearch } from './form' import { clearLocation } from './map' import { setActiveItinerary } from './narrative' -import { getUiUrlParams, getUrlParams } from '../util/query' +import { getUiUrlParams } from '../util/state' /** * Wrapper function for history#push that preserves the current search or, if @@ -103,7 +104,7 @@ export function handleBackButtonPress (e) { const uiUrlParams = getUiUrlParams(otpState) // Get new search ID from URL after back button pressed. // console.log('back button pressed', e) - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() const previousSearchId = urlParams.ui_activeSearch const previousItinIndex = +urlParams.ui_activeItinerary || 0 const previousSearch = otpState.searches[previousSearchId] diff --git a/lib/components/app/default-main-panel.js b/lib/components/app/default-main-panel.js index ed3de24b6..e200c01ef 100644 --- a/lib/components/app/default-main-panel.js +++ b/lib/components/app/default-main-panel.js @@ -14,10 +14,11 @@ class DefaultMainPanel extends Component { const { activeSearch, currentQuery, - customIcons, itineraryClass, itineraryFooter, + LegIcon, mainPanelContent, + ModeIcon, showUserSettings } = this.props const showPlanTripButton = mainPanelContent === 'EDIT_DATETIME' || @@ -35,7 +36,7 @@ class DefaultMainPanel extends Component { paddingBottom: 15, overflow: 'auto' }}> - + {!activeSearch && !showPlanTripButton && showUserSettings && } @@ -43,7 +44,8 @@ class DefaultMainPanel extends Component { + LegIcon={LegIcon} + />
{showPlanTripButton && diff --git a/lib/components/app/print-layout.js b/lib/components/app/print-layout.js index d6d126747..c140345d8 100644 --- a/lib/components/app/print-layout.js +++ b/lib/components/app/print-layout.js @@ -1,20 +1,19 @@ -import React, { Component } from 'react' +import PrintableItinerary from '@opentripplanner/printable-itinerary' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' -import BaseMap from '../map/base-map' -import EndpointsOverlay from '../map/endpoints-overlay' -import TransitiveOverlay from '../map/transitive-overlay' -import PrintableItinerary from '../narrative/printable/printable-itinerary' import { parseUrlQueryString } from '../../actions/form' import { routingQuery } from '../../actions/api' +import DefaultMap from '../map/default-map' +import TripDetails from '../narrative/connected-trip-details' import { getActiveItinerary } from '../../util/state' -import { getTimeFormat } from '../../util/time' class PrintLayout extends Component { static propTypes = { itinerary: PropTypes.object, + LegIcon: PropTypes.elementType.isRequired, parseQueryString: PropTypes.func } @@ -34,14 +33,14 @@ class PrintLayout extends Component { } componentDidMount () { - const { location } = this.props + const { location, parseUrlQueryString } = this.props // Add print-view class to html tag to ensure that iOS scroll fix only applies // to non-print views. const root = document.getElementsByTagName('html')[0] root.setAttribute('class', 'print-view') // Parse the URL query parameters, if present if (location && location.search) { - this.props.parseUrlQueryString() + parseUrlQueryString() } } @@ -54,7 +53,7 @@ class PrintLayout extends Component { } render () { - const { configCompanies, customIcons, itinerary, timeFormat } = this.props + const { config, itinerary, LegIcon } = this.props return (
{/* The header bar, including the Toggle Map and Print buttons */} @@ -74,21 +73,20 @@ class PrintLayout extends Component { {/* The map, if visible */} {this.state.mapVisible &&
- - - - +
} {/* The main itinerary body */} - {itinerary - ? - : null + {itinerary && + <> + + + }
) @@ -99,9 +97,8 @@ class PrintLayout extends Component { const mapStateToProps = (state, ownProps) => { return { - itinerary: getActiveItinerary(state.otp), - configCompanies: state.otp.config.companies, - timeFormat: getTimeFormat(state.otp.config) + config: state.otp.config, + itinerary: getActiveItinerary(state.otp) } } diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 9705cd327..eea5f49a7 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -1,20 +1,21 @@ -import React, { Component } from 'react' +import { ConnectedRouter } from 'connected-react-router' +import { createHashHistory } from 'history' +import isEqual from 'lodash.isequal' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { connect } from 'react-redux' -import isEqual from 'lodash.isequal' import { Route, Switch, withRouter } from 'react-router' -import { createHashHistory } from 'history' -import { ConnectedRouter } from 'connected-react-router' import PrintLayout from './print-layout' import { setMapCenter, setMapZoom } from '../../actions/config' -import { setLocationToCurrent } from '../../actions/map' -import { getCurrentPosition, receivedPositionResponse } from '../../actions/location' import { formChanged, parseUrlQueryString } from '../../actions/form' +import { getCurrentPosition, receivedPositionResponse } from '../../actions/location' +import { setLocationToCurrent } from '../../actions/map' import { handleBackButtonPress, matchContentToUrl } from '../../actions/ui' -import { getUrlParams } from '../../util/query' -import { getTitle, isMobile } from '../../util/ui' -import { getActiveItinerary } from '../../util/state' +import { getActiveItinerary, getTitle } from '../../util/state' + +const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { static propTypes = { @@ -29,7 +30,7 @@ class ResponsiveWebapp extends Component { componentDidUpdate (prevProps) { const { currentPosition, location, query, title } = this.props document.title = title - const urlParams = getUrlParams() + const urlParams = coreUtils.query.getUrlParams() const newSearchId = urlParams.ui_activeSearch // Determine if trip is being replanned by checking the active search ID // against the ID found in the URL params. If they are different, a new one @@ -55,7 +56,7 @@ class ResponsiveWebapp extends Component { // if in mobile mode and from field is not set, use current location as from and recenter map if (isMobile() && this.props.query.from === null) { - this.props.setLocationToCurrent({ type: 'from' }) + this.props.setLocationToCurrent({ locationType: 'from' }) this.props.setMapCenter(pt) if (this.props.initZoomOnLocate) { this.props.setMapZoom({ zoom: this.props.initZoomOnLocate }) diff --git a/lib/components/form/checkbox-selector.js b/lib/components/form/checkbox-selector.js deleted file mode 100644 index daabe0a6f..000000000 --- a/lib/components/form/checkbox-selector.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import { Form, FormGroup, Row, Col, Checkbox } from 'react-bootstrap' -import { connect } from 'react-redux' - -import { setQueryParam } from '../../actions/form' - -class CheckboxSelector extends Component { - static propTypes = { - name: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.bool - ]), - label: PropTypes.string, - setQueryParam: PropTypes.func - } - - _onQueryParamChange = (evt) => { - this.props.setQueryParam({ [this.props.name]: evt.target.checked }) - } - - render () { - const { label } = this.props - let value = this.props.value - if (typeof value === 'string') value = (value === 'true') - - return ( -
- - -
- - {label} - -
- -
-
- ) - } -} - -const mapStateToProps = (state, ownProps) => { - return { } -} - -const mapDispatchToProps = { setQueryParam } - -export default connect(mapStateToProps, mapDispatchToProps)(CheckboxSelector) diff --git a/lib/components/form/connected-location-field.js b/lib/components/form/connected-location-field.js new file mode 100644 index 000000000..1f497f84f --- /dev/null +++ b/lib/components/form/connected-location-field.js @@ -0,0 +1,85 @@ +import LocationField from '@opentripplanner/location-field' +import { + DropdownContainer, + FormGroup, + Input, + InputGroup, + InputGroupAddon, + MenuItemA +} from '@opentripplanner/location-field/lib/styled' +import { connect } from 'react-redux' +import styled from 'styled-components' + +import { clearLocation, onLocationSelected } from '../../actions/map' +import { addLocationSearch, getCurrentPosition } from '../../actions/location' +import { findNearbyStops } from '../../actions/api' +import { getActiveSearch, getShowUserSettings } from '../../util/state' + +const StyledLocationField = styled(LocationField)` + width: 100%; + + ${DropdownContainer} { + display: table-cell; + vertical-align: middle; + width: 1%; + } + + ${FormGroup} { + display: table; + padding: 6px 12px; + width: 100%; + } + + ${Input} { + display: table-cell; + padding: 6px 12px; + width: 100%; + } + + ${InputGroup} { + width: 100%; + } + + ${InputGroupAddon} { + display: table-cell; + vertical-align: middle; + width: 1%; + } + + ${MenuItemA} { + text-decoration: none; + } + + ${MenuItemA}:hover { + color: #333; + } +` + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + const { config, currentQuery, location, transitIndex, user } = state.otp + const { currentPosition, nearbyStops, sessionSearches } = location + const activeSearch = getActiveSearch(state.otp) + const query = activeSearch ? activeSearch.query : currentQuery + return { + currentPosition, + geocoderConfig: config.geocoder, + location: query[ownProps.locationType], + nearbyStops, + sessionSearches, + showUserSettings: getShowUserSettings(state.otp), + stopsIndex: transitIndex.stops, + userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] + } +} + +const mapDispatchToProps = { + addLocationSearch, + findNearbyStops, + getCurrentPosition, + onLocationSelected, + clearLocation +} + +export default connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) diff --git a/lib/components/form/connected-settings-selector-panel.js b/lib/components/form/connected-settings-selector-panel.js new file mode 100644 index 000000000..6d4f1d030 --- /dev/null +++ b/lib/components/form/connected-settings-selector-panel.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' + +import { setQueryParam } from '../../actions/form' +import { getShowUserSettings } from '../../util/state' + +import { StyledSettingsSelectorPanel } from './styled' +import UserTripSettings from './user-trip-settings' + +// TODO: Button title should be bold when button is selected. + +class ConnectedSettingsSelectorPanel extends Component { + static propTypes = { + ModeIcon: PropTypes.elementType.isRequired + } + + render () { + const { + config, + ModeIcon, + query, + setQueryParam, + showUserSettings + } = this.props + + return ( +
+
+ {showUserSettings && } + + +
+
+ ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + const { config, currentQuery } = state.otp + return { + query: currentQuery, + config, + showUserSettings: getShowUserSettings(state.otp) + } +} + +const mapDispatchToProps = { + setQueryParam +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConnectedSettingsSelectorPanel) diff --git a/lib/components/form/date-time-modal.js b/lib/components/form/date-time-modal.js index 74e839fa4..0d934e8e5 100644 --- a/lib/components/form/date-time-modal.js +++ b/lib/components/form/date-time-modal.js @@ -1,61 +1,37 @@ -// import necessary React/Redux libraries -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { connect } from 'react-redux' -import { Button, ButtonGroup } from 'react-bootstrap' -import DateTimeSelector from './date-time-selector' import { setQueryParam } from '../../actions/form' -// Define default routingType labels and components -const rtDefaults = [ - { - key: 'ITINERARY', - text: 'Itinerary', - component: - }, { - key: 'PROFILE', - text: 'Profile', - component: - } -] +import { StyledDateTimeSelector } from './styled' class DateTimeModal extends Component { static propTypes = { - routingType: PropTypes.string, setQueryParam: PropTypes.func } render () { - const { config, routingType, setQueryParam } = this.props + const { date, dateFormatLegacy, departArrive, setQueryParam, time, timeFormatLegacy } = this.props return (
- {/* The routing-type selection button row. Only show if more than one configured */} - {config.routingTypes.length > 1 && ( -
- - {config.routingTypes.map(rtConfig => { - return ( - - - - ) - })} - -
- )} - - {/* The main panel for the selected routing type */}
- {rtDefaults.find(d => d.key === routingType).component} + `. + // These props are not relevant in modern browsers, + // where `` already + // formats the time|date according to the OS settings. + dateFormatLegacy={dateFormatLegacy} + timeFormatLegacy={timeFormatLegacy} + />
) @@ -63,13 +39,16 @@ class DateTimeModal extends Component { } const mapStateToProps = (state, ownProps) => { - const {departArrive, date, time, routingType} = state.otp.currentQuery + const { departArrive, date, time } = state.otp.currentQuery + const config = state.otp.config return { - config: state.otp.config, + config, departArrive, date, time, - routingType + // These props below are for legacy browsers (see render method above). + timeFormatLegacy: coreUtils.time.getTimeFormat(config), + dateFormatLegacy: coreUtils.time.getDateFormat(config) } } diff --git a/lib/components/form/date-time-preview.js b/lib/components/form/date-time-preview.js index 08ce1d25b..846c8b974 100644 --- a/lib/components/form/date-time-preview.js +++ b/lib/components/form/date-time-preview.js @@ -1,15 +1,16 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' import moment from 'moment' -import { connect } from 'react-redux' +import coreUtils from '@opentripplanner/core-utils' +import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' -import { +const { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT, getTimeFormat, getDateFormat -} from '../../util/time' +} = coreUtils.time class DateTimePreview extends Component { static propTypes = { @@ -82,16 +83,17 @@ class DateTimePreview extends Component { const mapStateToProps = (state, ownProps) => { const { departArrive, date, time, routingType, startTime, endTime } = state.otp.currentQuery + const config = state.otp.config return { - config: state.otp.config, + config, routingType, departArrive, date, time, startTime, endTime, - timeFormat: getTimeFormat(state.otp.config), - dateFormat: getDateFormat(state.otp.config) + timeFormat: getTimeFormat(config), + dateFormat: getDateFormat(config) } } diff --git a/lib/components/form/date-time-selector.js b/lib/components/form/date-time-selector.js deleted file mode 100644 index e7a409816..000000000 --- a/lib/components/form/date-time-selector.js +++ /dev/null @@ -1,254 +0,0 @@ -// import moment from 'moment' -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import { Form, FormGroup, FormControl, Row, Col, Button } from 'react-bootstrap' -// import { SingleDatePicker } from 'react-dates' -import { connect } from 'react-redux' -import moment from 'moment' - -import { setQueryParam } from '../../actions/form' -import { - OTP_API_DATE_FORMAT, - OTP_API_TIME_FORMAT, - getTimeFormat, - getDateFormat -} from '../../util/time' - -function checkInput (type) { - var input = document.createElement('input') - input.setAttribute('type', type) - return input.type === type -} - -class DateTimeSelector extends Component { - static propTypes = { - date: PropTypes.string, - departArrive: PropTypes.string, - time: PropTypes.string, - location: PropTypes.object, - label: PropTypes.string, - profile: PropTypes.bool, - startTime: PropTypes.string, - endTime: PropTypes.string, - - setQueryParam: PropTypes.func, - type: PropTypes.string // replace with locationType? - } - - constructor (props) { - super(props) - this.state = { - dateFocused: false - } - this._supportsDateTimeInputs = checkInput('date') && checkInput('time') - console.log(`supports date time: ${this._supportsDateTimeInputs}`) - } - - _onDateChange = (evt) => { - this.props.setQueryParam({ date: evt.target.value }) - } - - _onDayOfWeekChange = evt => { - this.props.setQueryParam({ - date: moment().weekday(evt.target.value).format(OTP_API_DATE_FORMAT) - }) - } - - _onEndTimeChange = (evt) => { - this.props.setQueryParam({ endTime: evt.target.value }) - } - - _onStartTimeChange = (evt) => { - this.props.setQueryParam({ startTime: evt.target.value }) - } - - _onTimeChange = (evt) => { - this.props.setQueryParam({ time: evt.target.value }) - } - - _onBackupTimeChange = (evt) => { - const {setQueryParam, timeFormat} = this.props - const time = moment(evt.target.value, timeFormat).format(OTP_API_TIME_FORMAT) - setQueryParam({ time }) - } - - _onBackupDateChange = (evt) => { - const {setQueryParam, dateFormat} = this.props - const date = moment(evt.target.value, dateFormat).format(OTP_API_DATE_FORMAT) - setQueryParam({ date }) - } - - _setDepartArrive = (type) => { - const {setQueryParam} = this.props - setQueryParam({ departArrive: type }) - if (type === 'NOW') { - setQueryParam({ - date: moment().format(OTP_API_DATE_FORMAT), - time: moment().format(OTP_API_TIME_FORMAT) - }) - } - } - - render () { - const { departArrive, date, time, timeFormat, dateFormat } = this.props - - // TODO: restore for profile mode - /* if (this.props.profile) { - const dowOptions = [{ - text: 'WEEKDAY', - weekday: 3 - }, { - text: 'SATURDAY', - weekday: 6 - }, { - text: 'SUNDAY', - weekday: 0 - }] - - return ( -
- - - - - {dowOptions.map((o, i) => ( - - ))} - - - - - - - - TO - - - - - -
- ) - } */ - - return ( -
- - - {['NOW', 'DEPART', 'ARRIVE'].map((type, i) => ( - - - - ))} - - {departArrive !== 'NOW' && !this._supportsDateTimeInputs && ( - - - - - - - - - )} - {departArrive !== 'NOW' && this._supportsDateTimeInputs && ( - - - - - - - - - )} - -
- ) - } -} - -class DateOptionButton extends Component { - _onClick = () => { - this.props.setDepartArrive(this.props.type) - } - - render () { - const { active, type } = this.props - let text = type - if (type === 'NOW') text = 'Leave now' - if (type === 'DEPART') text = 'Depart at' - if (type === 'ARRIVE') text = 'Arrive by' - const classNames = ['date-option-button', 'select-button'] - if (active) classNames.push('active') - return - } -} - -const mapStateToProps = (state, ownProps) => { - const { departArrive, date, time, startTime, endTime } = state.otp.currentQuery - return { - config: state.otp.config, - departArrive, - date, - time, - startTime, - endTime, - timeFormat: getTimeFormat(state.otp.config), - dateFormat: getDateFormat(state.otp.config) - } -} - -const mapDispatchToProps = { - setQueryParam -} - -export default connect(mapStateToProps, mapDispatchToProps)(DateTimeSelector) diff --git a/lib/components/form/default-search-form.js b/lib/components/form/default-search-form.js index b6c72cd52..64dde53d2 100644 --- a/lib/components/form/default-search-form.js +++ b/lib/components/form/default-search-form.js @@ -1,19 +1,17 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' +import React, { Component } from 'react' -import LocationField from './location-field' -import SwitchButton from './switch-button' +import LocationField from './connected-location-field' import TabbedFormPanel from './tabbed-form-panel' -import defaultIcons from '../icons' +import SwitchButton from './switch-button' export default class DefaultSearchForm extends Component { static propTypes = { - icons: PropTypes.object, - mobile: PropTypes.bool + mobile: PropTypes.bool, + ModeIcon: PropTypes.elementType.isRequired } static defaultProps = { - icons: defaultIcons, showFrom: true, showTo: true } @@ -27,21 +25,21 @@ export default class DefaultSearchForm extends Component { } render () { - const { icons, mobile } = this.props + const { mobile, ModeIcon } = this.props const actionText = mobile ? 'tap' : 'click' return (
@@ -50,7 +48,7 @@ export default class DefaultSearchForm extends Component {
- + ) } diff --git a/lib/components/form/dropdown-selector.js b/lib/components/form/dropdown-selector.js deleted file mode 100644 index 3afeeced6..000000000 --- a/lib/components/form/dropdown-selector.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import { Form, FormGroup, FormControl, Row, Col } from 'react-bootstrap' -import { connect } from 'react-redux' - -import { setQueryParam } from '../../actions/form' - -class DropdownSelector extends Component { - static propTypes = { - name: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]), - label: PropTypes.string, - options: PropTypes.array, - setQueryParam: PropTypes.func - } - - _onQueryParamChange = (evt) => { - const val = evt.target.value - this.props.setQueryParam({ - [this.props.name]: isNaN(val) ? val : parseFloat(val) - }) - } - - render () { - const { value, label, options } = this.props - - return ( - - {label} - -
- - - {options.map((o, i) => ( - - ))} - - -
- -
- ) - } -} - -const mapStateToProps = (state, ownProps) => { - return { } -} - -const mapDispatchToProps = { setQueryParam } - -export default connect(mapStateToProps, mapDispatchToProps)(DropdownSelector) diff --git a/lib/components/form/general-settings-panel.js b/lib/components/form/general-settings-panel.js deleted file mode 100644 index bb8fe5a27..000000000 --- a/lib/components/form/general-settings-panel.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' - -import CheckboxSelector from './checkbox-selector' -import DropdownSelector from './dropdown-selector' -import queryParams from '../../util/query-params' -import { defaultParams, getQueryParamProperty } from '../../util/query' - -class GeneralSettingsPanel extends Component { - static propTypes = { - query: PropTypes.object, - paramNames: PropTypes.array - } - - static defaultProps = { - // The universe of properties to include in this form: - // TODO: allow override in config - paramNames: defaultParams - } - - render () { - const { paramNames, query, config } = this.props - return ( -
- {paramNames.map(param => { - const paramInfo = queryParams.find(qp => qp.name === param) - // Check that the parameter applies to the specified routingType - if (!paramInfo.routingTypes.includes(query.routingType)) return - - // Check that the applicability test (if provided) is satisfied - if (typeof paramInfo.applicable === 'function' && - !paramInfo.applicable(query, config)) return - - // Create the UI component based on the selector type - switch (paramInfo.selector) { - case 'DROPDOWN': - return - case 'CHECKBOX': - return - } - })} -
- ) - } -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - return { - config: state.otp.config, - query: state.otp.currentQuery - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(GeneralSettingsPanel) diff --git a/lib/components/form/location-field.js b/lib/components/form/location-field.js deleted file mode 100644 index 7c52c6e6b..000000000 --- a/lib/components/form/location-field.js +++ /dev/null @@ -1,605 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import { - Button, - FormGroup, - FormControl, - InputGroup, - DropdownButton, - MenuItem -} from 'react-bootstrap' -import { connect } from 'react-redux' -import { throttle } from 'throttle-debounce' - -import LocationIcon from '../icons/location-icon' -import { setLocation, setLocationToCurrent, clearLocation } from '../../actions/map' -import { addLocationSearch, getCurrentPosition } from '../../actions/location' -import { findNearbyStops } from '../../actions/api' -import { distanceStringImperial } from '../../util/distance' -import getGeocoder from '../../util/geocoder' -import { formatStoredPlaceName } from '../../util/map' -import { getActiveSearch, getShowUserSettings } from '../../util/state' -import { isIE } from '../../util/ui' - -class LocationField extends Component { - static propTypes = { - config: PropTypes.object, - currentPosition: PropTypes.object, - hideExistingValue: PropTypes.bool, - location: PropTypes.object, - label: PropTypes.string, - nearbyStops: PropTypes.array, - sessionSearches: PropTypes.array, - showClearButton: PropTypes.bool, - static: PropTypes.bool, // show autocomplete options as fixed/inline element rather than dropdown - stopsIndex: PropTypes.object, - type: PropTypes.string, // replace with locationType? - - // callbacks - onClick: PropTypes.func, - onLocationSelected: PropTypes.func, - - // dispatch - addLocationSearch: PropTypes.func, - clearLocation: PropTypes.func, - setLocation: PropTypes.func, - setLocationToCurrent: PropTypes.func - } - - static defaultProps = { - showClearButton: true - } - - constructor (props) { - super(props) - this.state = { - value: props.location && !props.hideExistingValue - ? props.location.name - : '', - menuVisible: false, - geocodedFeatures: [], - activeIndex: null - } - } - - componentDidUpdate (prevProps) { - // If location is updated externally, replace value and geocoded features - // in internal state. - // TODO: This might be considered an anti-pattern. There may be a more - // effective way to handle this. - const { location } = this.props - if (location !== prevProps.location) { - this.setState({ - value: location !== null ? location.name : '', - geocodedFeatures: [] - }) - } - } - - _geocodeAutocomplete = throttle(1000, (text) => { - if (!text) { - console.warn('No text entry provided for geocode autocomplete search.') - return - } - getGeocoder(this.props.config.geocoder) - .autocomplete({ text }) - .then((result) => { - this.setState({ geocodedFeatures: result.features }) - }).catch((err) => { - console.error(err) - }) - }) - - _geocodeSearch (text) { - if (!text) { - console.warn('No text entry provided for geocode search.') - return - } - getGeocoder(this.props.config.geocoder) - .search({ text }) - .then((result) => { - if (result.features && result.features.length > 0) { - // Only replace geocode items if results were found - this.setState({ geocodedFeatures: result.features }) - } else { - console.warn('No results found for geocode search. Not replacing results.') - } - }).catch((err) => { - console.error(err) - }) - } - - _getFormControlClassname () { - return this.props.type + '-form-control' - } - - _onClearButtonClick = () => { - const { type } = this.props - this.props.clearLocation({ type }) - this.setState({ - value: '', - geocodedFeatures: [] - }) - ReactDOM.findDOMNode(this.formControl).focus() - this._onTextInputClick() - } - - _onDropdownToggle = (v, e) => { - // if clicked on input form control, keep dropdown open; otherwise, toggle - const targetIsInput = - e.target.className.indexOf(this._getFormControlClassname()) !== -1 - const menuVisible = targetIsInput ? true : !this.state.menuVisible - this.setState({ menuVisible }) - } - /** - * Only hide menu if the target clicked is not a menu item in the dropdown. - * Otherwise, the click will not "finish" and the menu will hide without the - * user having made a selection. - */ - _onBlurFormGroup = (e) => { - // IE does not use relatedTarget, so this check handles cross-browser support. - // see https://stackoverflow.com/a/49325196/915811 - const target = e.relatedTarget !== null ? e.relatedTarget : document.activeElement - if (!this.props.location && (!target || target.getAttribute('role') !== 'menuitem')) { - this.setState({ menuVisible: false, value: '', geocodedFeatures: [] }) - } - } - - _onTextInputChange = (evt) => { - this.setState({ value: evt.target.value, menuVisible: true }) - this._geocodeAutocomplete(evt.target.value) - } - - _onTextInputClick = () => { - const { config, currentPosition, nearbyStops, onClick } = this.props - if (typeof onClick === 'function') onClick() - this.setState({ menuVisible: true }) - if (nearbyStops.length === 0 && currentPosition && currentPosition.coords) { - this.props.findNearbyStops({ - lat: currentPosition.coords.latitude, - lon: currentPosition.coords.longitude, - max: config.geocoder.maxNearbyStops || 4 - }) - } - } - - _onKeyDown = (evt) => { - const { activeIndex, menuVisible } = this.state - switch (evt.key) { - // 'Down' arrow key pressed: move selected menu item down by one position - case 'ArrowDown': - // Suppress default 'ArrowDown' behavior which moves cursor to end - evt.preventDefault() - if (!menuVisible) { - // If the menu is not visible, simulate a text input click to show it. - return this._onTextInputClick() - } - if (activeIndex === this.menuItemCount - 1) { - return this.setState({ activeIndex: null }) - } - return this.setState({ - activeIndex: activeIndex === null - ? 0 - : activeIndex + 1 - }) - - // 'Up' arrow key pressed: move selection up by one position - case 'ArrowUp': - // Suppress default 'ArrowUp' behavior which moves cursor to beginning - evt.preventDefault() - if (activeIndex === 0) { - return this.setState({ activeIndex: null }) - } - return this.setState({ - activeIndex: activeIndex === null - ? this.menuItemCount - 1 - : activeIndex - 1 - }) - - // 'Enter' keypress serves two purposes: - // - If pressed when typing in search string, switch from 'autocomplete' - // to 'search' geocoding - // - If pressed when dropdown results menu is active, apply the location - // associated with current selected menu item - case 'Enter': - if (typeof activeIndex === 'number') { // Menu is active - // Retrieve location selection handler from lookup object and invoke - const locationSelected = this.locationSelectedLookup[activeIndex] - if (locationSelected) locationSelected() - - // Clear selection & hide the menu - this.setState({ - menuVisible: false, - activeIndex: null - }) - } else { // Menu not active; get geocode 'search' results - this._geocodeSearch(evt.target.value) - // Ensure menu is visible. - this.setState({ menuVisible: true }) - } - - // Suppress default 'Enter' behavior which causes page to reload - evt.preventDefault() - break - case 'Escape': - // Clear selection & hide the menu - return this.setState({ - menuVisible: false, - activeIndex: null - }) - // Any other key pressed: clear active selection - default: - return this.setState({ activeIndex: null }) - } - } - - _setLocation (location) { - const { onLocationSelected, setLocation, type } = this.props - onLocationSelected && onLocationSelected() - setLocation({ type, location }) - } - - _useCurrentLocation = () => { - const { - currentPosition, - getCurrentPosition, - onLocationSelected, - setLocationToCurrent, - type - } = this.props - if (currentPosition.coords) { - // We already have geolocation coordinates - setLocationToCurrent({ type }) - onLocationSelected && onLocationSelected() - } else { - // Call geolocation.getCurrentPosition and set as from/to type - this.setState({ fetchingLocation: true }) - getCurrentPosition(type, onLocationSelected) - } - } - - /** - * Provide alert to user with reason for geolocation error - */ - _geolocationAlert = () => { - window.alert( - `Geolocation either has been disabled for ${window.location.host} or is not available in your browser.\n\nReason: ${this.props.currentPosition.error.message || 'Unknown reason'}` - ) - } - - render () { - const { - currentPosition, - label, - location, - user, - showClearButton, - showUserSettings, - static: isStatic, - suppressNearby, - type, - nearbyStops - } = this.props - const locations = [...user.locations, ...user.recentPlaces] - const { activeIndex } = this.state - let geocodedFeatures = this.state.geocodedFeatures - if (geocodedFeatures.length > 5) geocodedFeatures = geocodedFeatures.slice(0, 5) - - let sessionSearches = this.props.sessionSearches - if (sessionSearches.length > 5) sessionSearches = sessionSearches.slice(0, 5) - - // Assemble menu contents, to be displayed either as dropdown or static panel. - // Menu items are created in four phases: (1) the current location, (2) any - // geocoder search results; (3) nearby transit stops; and (4) saved searches - - let menuItems = [] // array of menu items for display (may include non-selectable items e.g. dividers/headings) - let itemIndex = 0 // the index of the current location-associated menu item (excluding non-selectable items) - this.locationSelectedLookup = {} // maps itemIndex to a location selection handler (for use by the _onKeyDown method) - - /* 1) Process geocode search result option(s) */ - if (geocodedFeatures.length > 0) { - // Add the menu sub-heading (not a selectable item) - // menuItems.push(Search Results) - - // Iterate through the geocoder results - menuItems = menuItems.concat(geocodedFeatures.map((feature, i) => { - // Create the selection handler - const locationSelected = () => { - getGeocoder(this.props.config.geocoder) - .getLocationFromGeocodedFeature(feature) - .then(location => { - // Set the current location - this._setLocation(location) - // Add to the location search history - this.props.addLocationSearch({ location }) - }) - } - - // Add to the selection handler lookup (for use in _onKeyDown) - this.locationSelectedLookup[itemIndex] = locationSelected - - // Create and return the option menu item - const option = createOption('map-pin', feature.properties.label, locationSelected, itemIndex === activeIndex, i === geocodedFeatures.length - 1) - itemIndex++ - return option - })) - } - - /* 2) Process nearby transit stop options */ - if (nearbyStops.length > 0 && !suppressNearby) { - // Add the menu sub-heading (not a selectable item) - menuItems.push(Nearby Stops) - - // Iterate through the found nearby stops - menuItems = menuItems.concat(nearbyStops.map((stopId, i) => { - // Constuct the location - const stop = this.props.stopsIndex[stopId] - const location = { - name: stop.name, - lat: stop.lat, - lon: stop.lon - } - - // Create the location selected handler - const locationSelected = () => { this._setLocation(location) } - - // Add to the selection handler lookup (for use in _onKeyDown) - this.locationSelectedLookup[itemIndex] = locationSelected - - // Create and return the option menu item - const option = createTransitStopOption(stop, locationSelected, itemIndex === activeIndex, i === nearbyStops.length - 1) - itemIndex++ - return option - })) - } - - /* 3) Process recent search history options */ - if (sessionSearches.length > 0) { - // Add the menu sub-heading (not a selectable item) - menuItems.push(Recently Searched) - - // Iterate through any saved locations - menuItems = menuItems.concat(sessionSearches.map((location, i) => { - // Create the location-selected handler - const locationSelected = () => { this._setLocation(location) } - - // Add to the selection handler lookup (for use in _onKeyDown) - this.locationSelectedLookup[itemIndex] = locationSelected - - // Create and return the option menu item - const option = createOption('search', location.name, locationSelected, itemIndex === activeIndex, i === sessionSearches.length - 1) - itemIndex++ - return option - })) - } - - /* 3b) Process stored user locations */ - if (locations.length > 0 && showUserSettings) { - // Add the menu sub-heading (not a selectable item) - menuItems.push(My Places) - - // Iterate through any saved locations - menuItems = menuItems.concat(locations.map((location, i) => { - // Create the location-selected handler - const locationSelected = () => { this._setLocation(location) } - - // Add to the selection handler lookup (for use in _onKeyDown) - this.locationSelectedLookup[itemIndex] = locationSelected - - // Create and return the option menu item - const option = createOption( - location.icon, - formatStoredPlaceName(location), - locationSelected, - itemIndex === activeIndex, - i === locations.length - 1 - ) - itemIndex++ - return option - })) - } - - /* 4) Process the current location */ - let locationSelected, optionIcon, optionTitle - - if (!currentPosition.error) { // current position detected successfully - locationSelected = this._useCurrentLocation - optionIcon = 'location-arrow' - optionTitle = 'Use Current Location' - } else { // error detecting current position - locationSelected = this._geolocationAlert - optionIcon = 'ban' - optionTitle = 'Current location not available' - } - - // Add to the selection handler lookup (for use in _onKeyDown) - this.locationSelectedLookup[itemIndex] = locationSelected - - if (!suppressNearby) { - // Create and add the option item to the menu items array - const currentLocationOption = createOption( - optionIcon, - optionTitle, - locationSelected, - itemIndex === activeIndex - ) - menuItems.push(currentLocationOption) - itemIndex++ - } - - // Store the number of location-associated items for reference in the _onKeyDown method - this.menuItemCount = itemIndex - - /** the text input element **/ - const placeholder = currentPosition.fetching === type - ? 'Fetching location...' - : label || type - const textControl = { this.formControl = ctl }} - className={this._getFormControlClassname()} - type='text' - value={this.state.value} - placeholder={placeholder} - onChange={this._onTextInputChange} - onClick={this._onTextInputClick} - onKeyDown={this._onKeyDown} - /> - - // Only include the clear ('X') button add-on if a location is selected - // or if the input field has text. - const clearButton = showClearButton && location - ? - - - : null - if (isStatic) { - // 'static' mode (menu is displayed alongside input, e.g., for mobile view) - return ( -
-
- - - - - - {textControl} - {clearButton} - - -
-
    - {menuItems.length > 0 // Show typing prompt to avoid empty screen - ? menuItems - : - Begin typing to search for locations - - } -
-
- ) - } else { - // default display mode with dropdown menu - return ( -
- - - {/* location field icon -- also serves as dropdown anchor */} - } - noCaret - > - {menuItems} - - {textControl} - {clearButton} - - -
- ) - } - } -} - -// helper functions for dropdown options - -let itemKey = 0 - -function createOption (icon, title, onSelect, isActive, isLast) { - return - {isIE() - // In internet explorer 11, some really weird stuff is happening where it - // is not possible to click the text of the title, but if you click just - // above it, then it works. So, if using IE 11, just return the title text - // and avoid all the extra fancy stuff. - // See https://github.com/ibi-group/trimet-mod-otp/issues/237 - ? title - : ( -
-
-
- {title} -
-
- ) - } -
-} - -function createTransitStopOption (stop, onSelect, isActive) { - return -
-
- -
{distanceStringImperial(stop.dist, true)}
-
-
-
{stop.name} ({stop.code})
-
- {(stop.routes || []).map((route, i) => { - const name = route.shortName || route.longName - return ( - - {name} - - ) - })} -
-
-
-
- -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - const query = activeSearch ? activeSearch.query : state.otp.currentQuery - const location = query[ownProps.type] - const showUserSettings = getShowUserSettings(state.otp) - return { - config: state.otp.config, - location, - user: state.otp.user, - currentPosition: state.otp.location.currentPosition, - sessionSearches: state.otp.location.sessionSearches, - nearbyStops: state.otp.location.nearbyStops, - showUserSettings, - stopsIndex: state.otp.transitIndex.stops - } -} - -const mapDispatchToProps = { - addLocationSearch, - findNearbyStops, - getCurrentPosition, - setLocation, - setLocationToCurrent, - clearLocation -} - -export default connect(mapStateToProps, mapDispatchToProps)(LocationField) diff --git a/lib/components/form/mode-button.js b/lib/components/form/mode-button.js deleted file mode 100644 index 58771ec20..000000000 --- a/lib/components/form/mode-button.js +++ /dev/null @@ -1,153 +0,0 @@ -import React, {Component, PureComponent} from 'react' -import PropTypes from 'prop-types' - -import { getIcon, isTransit } from '../../util/itinerary' - -export default class ModeButton extends Component { - static propTypes = { - active: PropTypes.bool, - label: PropTypes.string, - mode: PropTypes.any, // currently a mode object or string - icons: PropTypes.object, - onClick: PropTypes.func - } - - _getButtonStyle ({ - active, - enabled, - height, - modeStr - }) { - const buttonStyle = { height } - - if (modeStr !== 'TRANSIT' && isTransit(modeStr)) { - buttonStyle.width = height - buttonStyle.border = `2px solid ${enabled ? (active ? '#000' : '#bbb') : '#ddd'}` - if (active && enabled) buttonStyle.backgroundColor = '#fff' - buttonStyle.borderRadius = height / 2 - } else { - buttonStyle.border = active ? '2px solid #000' : '1px solid #bbb' - if (active) buttonStyle.backgroundColor = '#add8e6' - } - - return buttonStyle - } - - render () { - const { - active, - enabled, - icons, - label, - mode, - onClick, - inlineLabel, - showPlusTransit - } = this.props - const height = this.props.height || 48 - const iconSize = height - 20 - const iconColor = enabled ? '#000' : '#ccc' - const modeStr = mode.company || mode.mode || mode - const buttonStyle = this._getButtonStyle({ active, enabled, height, modeStr }) - - return ( -
- - - {/* If not in inline-label mode, label directly below the button */} - {!inlineLabel && ( -
- {label} -
- )} -
- ) - } -} - -class PlusTransit extends PureComponent { - render () { - const {enabled, iconColor, icons, iconSize} = this.props - return ( - -
- {enabled - ? getIcon('TRANSIT', icons) - : ( -
- ) - } -
- - - ) - } -} diff --git a/lib/components/form/mode-selector.js b/lib/components/form/mode-selector.js deleted file mode 100644 index 5f8abe3a9..000000000 --- a/lib/components/form/mode-selector.js +++ /dev/null @@ -1,70 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { FormGroup, ControlLabel, FormControl } from 'react-bootstrap' -import { connect } from 'react-redux' - -import { setQueryParam } from '../../actions/form' - -class ModeSelector extends Component { - static propTypes = { - config: PropTypes.object, - label: PropTypes.string, - mode: PropTypes.string, - setQueryParam: PropTypes.func, - showLabel: PropTypes.bool - } - - static defaultProps = { - label: 'Mode', - showLabel: true - } - - _onChange = (evt) => this.props.setQueryParam({ mode: evt.target.value }) - - _getDisplayText (mode) { - switch (mode) { - case 'TRANSIT,WALK': return 'Walk to Transit' - case 'TRANSIT,BICYCLE': return 'Bike to Transit' - case 'WALK': return 'Walk Only' - case 'BICYCLE': return 'Bike Only' - } - return mode - } - - render () { - const { config, mode, label, showLabel } = this.props - - return ( -
- - {showLabel - ? {label} - : null - } - - {config.modes.map((m, i) => ( - - ))} - - -
- ) - } -} - -const mapStateToProps = (state, ownProps) => { - return { - config: state.otp.config, - mode: state.otp.currentQuery.mode - } -} - -const mapDispatchToProps = { - setQueryParam -} - -export default connect(mapStateToProps, mapDispatchToProps)(ModeSelector) diff --git a/lib/components/form/modes-panel.js b/lib/components/form/modes-panel.js deleted file mode 100644 index bbfb4dd5b..000000000 --- a/lib/components/form/modes-panel.js +++ /dev/null @@ -1,152 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' - -import { setQueryParam } from '../../actions/form' -import ModeButton from './mode-button' -import { isAccessMode } from '../../util/itinerary' - -class ModesPanel extends Component { - static propTypes = { - icons: PropTypes.object, - modeGroups: PropTypes.array, - queryModes: PropTypes.array, - setQueryParam: PropTypes.func - } - - _getVisibleModes (group) { - // Don't show the CAR_HAIL services in profile modes - // TODO: this could be handled more elegantly? - return group.modes.filter(mode => - mode.mode !== 'CAR_HAIL' || this.props.routingType !== 'PROFILE' - ) - } - - // Returns whether a particular mode or TNC agency is active - _modeIsActive (mode) { - const { companies, queryModes } = this.props - if (mode.mode === 'CAR_HAIL') { - return Boolean(companies && companies.includes(mode.label.toUpperCase())) - } else { - return queryModes.includes(mode.mode || mode) - } - } - - _setGroupSelected (group, isSelected) { - let queryModes = this.props.queryModes.slice(0) // Clone the modes array - - this._getVisibleModes(group).forEach(mode => { - const modeStr = mode.mode || mode - queryModes = queryModes.filter(m => m !== modeStr) - if (isSelected) queryModes.push(modeStr) - }) - - // Update the mode array in the store - this.props.setQueryParam({ mode: queryModes.join(',') }) - } - - _toggleMode (mode) { - const modeStr = mode.mode || mode - - const { routingType, setQueryParam } = this.props - let queryModes = this.props.queryModes.slice(0) // Clone the modes array - - const queryParamUpdate = {} - - // Special case: we are in ITINERARY mode and changing the one access mode - if (routingType === 'ITINERARY' && isAccessMode(modeStr)) { - queryModes = queryModes.filter(m => !isAccessMode(m)) - queryModes.push(modeStr) - - // do extra stuff if mode selected was a TNC - queryParamUpdate.companies = modeStr === 'CAR_HAIL' ? mode.label.toUpperCase() : null - - // Otherwise, if mode is currently selected, deselect it - } else if (queryModes.includes(modeStr)) { - queryModes = queryModes.filter(m => m !== modeStr) - - // Or, if mode is currently not selected, select it - } else if (!queryModes.includes(modeStr)) { - queryModes.push(modeStr) - } - - queryParamUpdate.mode = queryModes.join(',') - - // Update the mode array in the store - setQueryParam(queryParamUpdate) - } - - render () { - const { icons, modeGroups, routingType } = this.props - - return ( -
- {modeGroups.map((group, k) => { - const groupModes = this._getVisibleModes(group) - // Determine whether to show Select/Deselect All actions - const accessCount = groupModes.filter(m => isAccessMode(m.mode || m)).length - const showGroupSelect = - (routingType === 'PROFILE' || - (routingType === 'ITINERARY' && accessCount === 0)) && - groupModes.length > 1 - - return ( -
-
- {showGroupSelect && ( -
- {' '}|{' '} - -
- )} -
{group.name}
-
-
- {groupModes.map(mode => { - return this._toggleMode(mode)} - /> - })} -
-
- ) - })} -
- ) - } -} - -// Make a mode string more readable (e.g. 'BICYCLE_RENT' -> 'Bicycle Rent') -function readableModeString (mode) { - const str = mode.replace('_', ' ') - return str.replace(/\w\S*/g, txt => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() }) -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - const { companies, mode, routingType } = state.otp.currentQuery - return { - companies, - modeGroups: state.otp.config.modeGroups, - queryModes: !mode || mode.length === 0 ? [] : mode.split(','), - routingType - } -} - -const mapDispatchToProps = { setQueryParam } - -export default connect(mapStateToProps, mapDispatchToProps)(ModesPanel) diff --git a/lib/components/form/plan-trip-button.js b/lib/components/form/plan-trip-button.js index b619e647b..e2a091d0c 100644 --- a/lib/components/form/plan-trip-button.js +++ b/lib/components/form/plan-trip-button.js @@ -1,11 +1,11 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import { routingQuery } from '../../actions/api' import { setMainPanelContent } from '../../actions/ui' -import { isMobile } from '../../util/ui' class PlanTripButton extends Component { static propTypes = { @@ -23,7 +23,7 @@ class PlanTripButton extends Component { _onClick = () => { this.props.routingQuery() if (typeof this.props.onClick === 'function') this.props.onClick() - if (!isMobile()) this.props.setMainPanelContent(null) + if (!coreUtils.ui.isMobile()) this.props.setMainPanelContent(null) } render () { diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 1cefa4d98..b39baeda6 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -1,17 +1,15 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import { isNotDefaultQuery } from '../../util/query' - class SettingsPreview extends Component { static propTypes = { // component props caret: PropTypes.string, compressed: PropTypes.bool, editButtonText: PropTypes.element, - icons: PropTypes.object, showCaret: PropTypes.bool, onClick: PropTypes.func, @@ -28,7 +26,7 @@ class SettingsPreview extends Component { render () { const { config, query, caret, editButtonText } = this.props // Show dot indicator if the current query differs from the default query. - let showDot = isNotDefaultQuery(query, config) + let showDot = coreUtils.query.isNotDefaultQuery(query, config) const button = (
- })} -
-
-
- ) - } - } - - _renderExclusiveAccessSelectors = () => { - const {config, mode, icons} = this.props - const {exclusiveModes} = config.modes - const modeHasTransit = hasTransit(mode) - // Use int for array element keys - let key = 0 - if (!exclusiveModes) return null - - // create an array of children to display within a mode-group-row - // at most 2 exclusive modes will be displayed side-by-side - const children = [] - const spacer = () => ( -   - ) - - exclusiveModes.forEach((exclusiveMode, idx) => { - // add left padding for every evenly indexed exclusiveMode - if (idx % 2 === 0) { - children.push(spacer()) - } - - switch (exclusiveMode) { - case 'WALK': - children.push( - - - - ) - break - case 'BICYCLE': - children.push( - - - - ) - break - case 'MICROMOBILITY': - children.push( - - - - ) - break - default: - throw new Error(`Unsupported exclusive mode: ${exclusiveMode}`) - } - - // add right padding for every odd indexed exclusiveMode - if (idx % 2 !== 0) { - children.push(spacer()) - } - }) - - return ( - - {children} - - ) - } - - render () { - const { - config, - defaults, - mode, - icons, - query, - queryModes, - showUserSettings - } = this.props - const modeHasTransit = hasTransit(mode) - const { transitModes, accessModes, bicycleModes, micromobilityModes } = config.modes - - // Do not permit remembering trip options if they do not differ from the - // defaults and nothing has been stored - const queryIsDefault = !isNotDefaultQuery(query, config) - const rememberIsDisabled = queryIsDefault && !defaults - - return ( -
-
- {showUserSettings && -
- - -
- } - {/* Take Transit button */} - - - this._setAccessMode('WALK')} - /> - - - - {/* transit access mode selector */} - - {accessModes.map((mode, k) => { - return - this._setAccessMode(mode)} - /> - - })} - - - {this._renderExclusiveAccessSelectors()} - - {/* Transit mode selector */} - {/* - -
-
Filter Transit Modes
-
- - - {transitModes.map((mode, k) => { - return (
- this._toggleTransitMode(mode)} - /> -
) - })} - -
*/} - -
- - {/* Travel Preferences */} - - -
Travel Preferences
- - {/* The bike trip type selector */} - {hasBike(mode) && !hasTransit(mode) && ( -
-
Use
-
- {bicycleModes.map((option, k) => { - let action = this._setOwnBike - if (option.mode === 'BICYCLE_RENT') action = this._setRentedBike - let classNames = ['select-button'] - if (queryModes.includes(option.mode)) classNames.push('active') - // TODO: Handle different bikeshare networks - return ( - - ) - })} -
-
- )} - - {/* The micromobility trip type selector */} - {hasMicromobility(mode) && !hasTransit(mode) && ( -
-
Use
-
- {micromobilityModes.map((option, k) => { - let action = this._setOwnMicromobility - if (option.mode === 'MICROMOBILITY_RENT') action = this._setRentedMicromobility - let classNames = ['select-button'] - if (queryModes.includes(option.mode)) classNames.push('active') - // TODO: Handle different bikeshare networks - return ( - - ) - })} -
-
- )} - - {this._renderCompanies()} - - {/* The transit mode selected */} - {hasTransit(mode) && (
-
Use
-
- {transitModes.map((mode, k) => { - let classNames = ['select-button'] - if (this._modeIsActive(mode)) classNames.push('active') - return - })} -
-
-
)} - - {/* Other general settings */} - - - -
- ) - } -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - const { config, currentQuery, user } = state.otp - const { defaults } = user - const showUserSettings = getShowUserSettings(state.otp) - const { companies, mode, routingType } = currentQuery - return { - defaults, - query: currentQuery, - config, - mode, - companies, - modeGroups: config.modeGroups, - queryModes: !mode || mode.length === 0 ? [] : mode.split(','), - routingType, - showUserSettings - } -} - -const mapDispatchToProps = { - clearDefaultSettings, - resetForm, - setQueryParam, - storeDefaultSettings -} - -export default connect(mapStateToProps, mapDispatchToProps)(SettingsSelectorPanel) diff --git a/lib/components/form/styled.js b/lib/components/form/styled.js new file mode 100644 index 000000000..3d0909694 --- /dev/null +++ b/lib/components/form/styled.js @@ -0,0 +1,194 @@ +import styled, { css } from 'styled-components' +import { DateTimeSelector, SettingsSelectorPanel } from '@opentripplanner/trip-form' +import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' + +const commonButtonCss = css` + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + background: none; + border: 1px solid rgb(187, 187, 187); + border-radius: 3px; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + outline-offset: -2px; + padding: 6px 12px; + text-align: center; + touch-action: manipulation; + user-select: none; + + &.active { + background-color: rgb(173, 216, 230); + border: 2px solid rgb(0, 0, 0); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + font-weight: 600; + } +` + +const commonInputCss = css` + background: none; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + color: #555; + font-family: inherit; + font-weight: inherit; + padding: 6px 12px; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +` + +const modeButtonButtonCss = css` + ${TripFormClasses.ModeButton.Button} { + ${commonButtonCss} + } +` + +export const StyledSettingsSelectorPanel = styled(SettingsSelectorPanel)` + ${modeButtonButtonCss} + + ${TripFormClasses.SettingLabel} { + color: #808080; + font-size: 14px; + font-weight: 100; + letter-spacing: 1px; + padding-top: 8px; + text-transform: uppercase; + } + ${TripFormClasses.SettingsHeader} { + color: #333333; + font-size: 18px; + margin: 16px 0px; + } + ${TripFormClasses.SettingsSection} { + margin-bottom: 16px; + } + ${TripFormClasses.DropdownSelector} { + select { + ${commonInputCss} + -webkit-appearance: none; + border-radius: 3px; + font-size: 14px; + height: 34px; + line-height: 1.42857; + margin-bottom: 20px; + + &:focus { + border-color: #66afe9; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + outline: 0; + } + } + > div:last-child::after { + box-sizing: border-box; + color: #000; + content: "â–¼"; + font-size: 67%; + pointer-events: none; + position: absolute; + right: 8px; + top: 10px; + } + } + + ${TripFormClasses.ModeSelector} { + font-weight: 300; + ${TripFormClasses.ModeButton.Button} { + box-shadow: none; + outline: none; + padding: 3px; + } + ${TripFormClasses.ModeButton.Title} { + font-size: 10px; + line-height: 12px; + padding: 4px 0px 0px; + + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.MainRow} { + box-sizing: border-box; + font-size: 170%; + margin: 0px -10px 18px; + padding: 0px 5px; + ${TripFormClasses.ModeButton.Button} { + height: 54px; + width: 100%; + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.SecondaryRow} { + margin: 0px -10px 10px; + ${TripFormClasses.ModeButton.Button} { + font-size: 130%; + font-weight: 800; + height: 46px; + > svg { + margin: 0 0.20em; + } + } + } + ${TripFormClasses.ModeSelector.TertiaryRow} { + font-size: 80%; + font-weight: 300; + margin: 0px -10px 10px; + text-align: center; + ${TripFormClasses.ModeButton.Button} { + height: 36px; + } + } + ${TripFormClasses.SubmodeSelector.Row} { + font-size: 12px; + > * { + padding: 3px 5px 3px 0px; + } + > :last-child { + padding-right: 0px; + } + ${TripFormClasses.ModeButton.Button} { + height: 35px; + } + svg, + img { + margin-left: 0px; + } + } + ${TripFormClasses.SubmodeSelector} { + ${TripFormClasses.SettingLabel} { + margin-bottom: 0; + } + } + ${TripFormClasses.SubmodeSelector.InlineRow} { + margin: -3px 0px; + svg, + img { + height: 18px; + max-width: 32px; + } + } +` + +export const StyledDateTimeSelector = styled(DateTimeSelector)` + margin: 0 -15px 15px; + + ${TripFormClasses.DateTimeSelector.DateTimeRow} { + margin: 20px 0px 15px; + input { + ${commonInputCss} + background-color: #fff; + border: 0; + border-bottom: 1px solid #000; + box-shadow: none; + outline: none; + text-align: center; + } + } + ${TripFormClasses.ModeButton.Button} { + ${commonButtonCss} + font-size: 14px; + height: 35px; + } +` diff --git a/lib/components/form/tabbed-form-panel.js b/lib/components/form/tabbed-form-panel.js index c3fededce..2452eadb4 100644 --- a/lib/components/form/tabbed-form-panel.js +++ b/lib/components/form/tabbed-form-panel.js @@ -6,13 +6,13 @@ import { connect } from 'react-redux' import DateTimePreview from './date-time-preview' import SettingsPreview from './settings-preview' import DateTimeModal from './date-time-modal' -import SettingsSelectorPanel from './settings-selector-panel' +import ConnectedSettingsSelectorPanel from './connected-settings-selector-panel' import { setMainPanelContent } from '../../actions/ui' class TabbedFormPanel extends Component { static propTypes = { - icons: PropTypes.object + ModeIcon: PropTypes.elementType.isRequired } _onEditDateTimeClick = () => { @@ -28,7 +28,7 @@ class TabbedFormPanel extends Component { _onHideClick = () => this.props.setMainPanelContent(null) render () { - const { icons, mainPanelContent } = this.props + const { ModeIcon, mainPanelContent } = this.props return (
@@ -49,7 +49,7 @@ class TabbedFormPanel extends Component { {(mainPanelContent === 'EDIT_DATETIME' || mainPanelContent === 'EDIT_SETTINGS') && (
{mainPanelContent === 'EDIT_DATETIME' && ()} - {mainPanelContent === 'EDIT_SETTINGS' && ()} + {mainPanelContent === 'EDIT_SETTINGS' && ()}
+ +
+ ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + const { config, currentQuery, user } = state.otp + const { defaults } = user + + return { + config, + defaults, + query: currentQuery + } +} + +const mapDispatchToProps = { + clearDefaultSettings, + resetForm, + storeDefaultSettings +} + +export default connect(mapStateToProps, mapDispatchToProps)(UserTripSettings) diff --git a/lib/components/icons/bike-icon.js b/lib/components/icons/bike-icon.js deleted file mode 100644 index c97d0de98..000000000 --- a/lib/components/icons/bike-icon.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, { Component } from 'react' - -export default class BikeIcon extends Component { - render () { - return ( - - - - - - - - - - - - ) - } -} diff --git a/lib/components/icons/biketown-icon.js b/lib/components/icons/biketown-icon.js deleted file mode 100644 index 50fcefac7..000000000 --- a/lib/components/icons/biketown-icon.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { Component } from 'react' - -export default class BiketownIcon extends Component { - render () { - return ( - - - - - - - - - - - ) - } -} diff --git a/lib/components/icons/bus-icon.js b/lib/components/icons/bus-icon.js deleted file mode 100644 index 64bbcaf71..000000000 --- a/lib/components/icons/bus-icon.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from 'react' - -export default class BusIcon extends Component { - render () { - return ( - - - - - - - - - - - - - - - - ) - } -} diff --git a/lib/components/icons/car2go-icon.js b/lib/components/icons/car2go-icon.js deleted file mode 100644 index fe4c01f3d..000000000 --- a/lib/components/icons/car2go-icon.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from 'react' - -export default class Car2goIcon extends Component { - render () { - return ( - > - - - - - - - - ) - } -} diff --git a/lib/components/icons/direction-icon.js b/lib/components/icons/direction-icon.js deleted file mode 100644 index 3203d1e1c..000000000 --- a/lib/components/icons/direction-icon.js +++ /dev/null @@ -1,82 +0,0 @@ -import React, { Component } from 'react' - -export default class DirectionIcon extends Component { - render () { - const { relativeDirection } = this.props - if (!relativeDirection) return null - switch (relativeDirection.toUpperCase()) { - case 'DEPART': - case 'CONTINUE': return ( - - - - ) - case 'LEFT': return ( - - - - ) - case 'RIGHT': return ( - - - - ) - case 'SLIGHTLY_LEFT': return ( - - - - ) - case 'SLIGHTLY_RIGHT': return ( - - - - ) - case 'HARD_LEFT': return ( - - - - ) - case 'HARD_RIGHT': return ( - - - - ) - case 'UTURN_LEFT': return ( - - - - ) - case 'UTURN_RIGHT': return ( - - - - ) - case 'CIRCLE_CLOCKWISE': return ( - - - - - - - - - ) - case 'CIRCLE_COUNTERCLOCKWISE': return ( - - - - - - - - - ) - case 'ELEVATOR': return ( - - - - ) - } - return null - } -} diff --git a/lib/components/icons/gondola-icon.js b/lib/components/icons/gondola-icon.js deleted file mode 100644 index d14bcb744..000000000 --- a/lib/components/icons/gondola-icon.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from 'react' - -export default class GondolaIcon extends Component { - render () { - return ( - - - - - ) - } -} diff --git a/lib/components/icons/index.js b/lib/components/icons/index.js deleted file mode 100644 index 8bdf35517..000000000 --- a/lib/components/icons/index.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' - -import BikeIcon from './bike-icon' -import BiketownIcon from './biketown-icon' -import BusIcon from './bus-icon' -import Car2goIcon from './car2go-icon' -import ReachNowIcon from './reachnow-icon' -import GondolaIcon from './gondola-icon' -import LyftIcon from './lyft-icon' -import RailIcon from './rail-icon' -import StreetcarIcon from './streetcar-icon' -import TramIcon from './tram-icon' -import TransitIcon from './transit-icon' -import UberIcon from './uber-icon' -import WalkIcon from './walk-icon' - -// define Portland-specific mode icons -export default { - BICYCLE: , - BICYCLE_RENT: , - BUS: , - CAR_HAIL_LYFT: , - CAR_HAIL_UBER: , - CAR_RENT_CAR2GO: , - CAR_RENT_REACHNOW: , - GONDOLA: , - RAIL: , - STREETCAR: , - TRAM: , - TRANSIT: , - WALK: , - customModeForLeg: (leg) => { - if (leg.routeLongName && leg.routeLongName.startsWith('Portland Streetcar')) return 'STREETCAR' - return null - } -} diff --git a/lib/components/icons/location-icon.js b/lib/components/icons/location-icon.js deleted file mode 100644 index d1c245e72..000000000 --- a/lib/components/icons/location-icon.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class LocationIcon extends Component { - static propTypes = { - type: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object - } - - render () { - const { type, className, style } = this.props - - // from: #f5a81c - // to: '#8ec449 - - let classNameArr = ['fa', `${type}-location-icon`] - if (type === 'from') classNameArr.push('fa-dot-circle-o') - else if (type === 'to') classNameArr.push('fa-map-marker') - if (className) classNameArr = classNameArr.concat(className.split(' ')) - - return - } -} diff --git a/lib/components/icons/lyft-icon.js b/lib/components/icons/lyft-icon.js deleted file mode 100644 index add6801bf..000000000 --- a/lib/components/icons/lyft-icon.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from 'react' - -export default class LyftIcon extends Component { - render () { - return ( - - - - ) - } -} diff --git a/lib/components/icons/mode-icon.js b/lib/components/icons/mode-icon.js deleted file mode 100644 index dbbd75b54..000000000 --- a/lib/components/icons/mode-icon.js +++ /dev/null @@ -1,129 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class ModeIcon extends Component { - static propTypes = { - mode: PropTypes.string - } - - render () { - if (!this.props.mode) return null - switch (this.props.mode.toLowerCase()) { - case 'bus': return - case 'tram': - case 'rail': - case 'subway': - return - case 'walk': return - case 'bicycle': - case 'bicycle_rent': - return - case 'ferry': return - case 'gondola': return - case 'car': return - case 'micromobility': - case 'micromobility_rent': - return - } - return null - } -} - -class BusIcon extends Component { - render () { - return ( - - - - - - - - - ) - } -} - -class TramIcon extends Component { - render () { - return ( - - - - - - ) - } -} - -class WalkIcon extends Component { - render () { - return ( - - - - - ) - } -} - -class BicycleIcon extends Component { - render () { - return ( - - - - ) - } -} - -class FerryIcon extends Component { - render () { - return ( - - - - - - - ) - } -} - -class GondolaIcon extends Component { - render () { - return ( - - - - - - - ) - } -} - -class CarIcon extends Component { - render () { - return ( - - - - ) - } -} - -/** - * Icons made by Freepik (https://www.freepik.com/) - * from Flaticon (https://www.flaticon.com/) - * licensed by Creative Commons BY 3.0 (http://creativecommons.org/licenses/by/3.0/) - */ -class MicromobilityIcon extends Component { - render () { - return ( - - - - ) - } -} diff --git a/lib/components/icons/rail-icon.js b/lib/components/icons/rail-icon.js deleted file mode 100644 index 8f2ab99d6..000000000 --- a/lib/components/icons/rail-icon.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from 'react' - -export default class RailIcon extends Component { - render () { - return ( - - - - ) - } -} diff --git a/lib/components/icons/reachnow-icon.js b/lib/components/icons/reachnow-icon.js deleted file mode 100644 index bab628729..000000000 --- a/lib/components/icons/reachnow-icon.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from 'react' - -export default class ReachNowIcon extends Component { - render () { - return ( - - - - - - - ) - } -} diff --git a/lib/components/icons/streetcar-icon.js b/lib/components/icons/streetcar-icon.js deleted file mode 100644 index e53550142..000000000 --- a/lib/components/icons/streetcar-icon.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from 'react' - -export default class StreetcarIcon extends Component { - render () { - return ( - - - - - ) - } -} diff --git a/lib/components/icons/tram-icon.js b/lib/components/icons/tram-icon.js deleted file mode 100644 index 10e69e931..000000000 --- a/lib/components/icons/tram-icon.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from 'react' - -export default class TramIcon extends Component { - render () { - return ( - - - - - ) - } -} diff --git a/lib/components/icons/transit-icon.js b/lib/components/icons/transit-icon.js deleted file mode 100644 index d2e96487c..000000000 --- a/lib/components/icons/transit-icon.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from 'react' - -export default class TransitIcon extends Component { - render () { - // TODO: Find a better general transit icon to use than the bus icon. - return ( - - - - - - - - - - - - - - - ) - } -} diff --git a/lib/components/icons/uber-icon.js b/lib/components/icons/uber-icon.js deleted file mode 100644 index 77daea91a..000000000 --- a/lib/components/icons/uber-icon.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from 'react' - -export default class UberIcon extends Component { - render () { - return ( - - - - - - - ) - } -} diff --git a/lib/components/icons/walk-icon.js b/lib/components/icons/walk-icon.js deleted file mode 100644 index a071dcfa4..000000000 --- a/lib/components/icons/walk-icon.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from 'react' - -export default class WalkIcon extends Component { - render () { - return ( - - - - - - - ) - } -} diff --git a/lib/components/map/base-map.js b/lib/components/map/base-map.js deleted file mode 100644 index bb648001a..000000000 --- a/lib/components/map/base-map.js +++ /dev/null @@ -1,421 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import isEqual from 'lodash.isequal' - -import { Map, TileLayer, LayersControl, Popup, CircleMarker } from 'react-leaflet' - -import { setLocation, setMapPopupLocation, setMapPopupLocationAndGeocode } from '../../actions/map' -import { setMapZoom, updateOverlayVisibility } from '../../actions/config' -import LocationIcon from '../icons/location-icon' -import { constructLocation } from '../../util/map' -import { getActiveItinerary, getActiveSearch } from '../../util/state' -import { getItineraryBounds, getLegBounds, legLocationAtDistance } from '../../util/itinerary' -import { isMobile } from '../../util/ui' - -import L from 'leaflet' - -L.Evented.addInitHook(function () { - if (this) this._singleClickTimeout = null - this.on('click', this._scheduleSingleClick, this) - this.on('dblclick dragstart zoomstart', this._cancelSingleClick, this) -}) - -L.Evented.include({ - _cancelSingleClick: function () { - // This timeout is key to workaround an issue where double-click events - // are fired in this order on some touch browsers: ['click', 'dblclick', 'click'] - // instead of ['click', 'click', 'dblclick'] - setTimeout(this._clearSingleClickTimeout.bind(this), 0) - }, - - _scheduleSingleClick: function (e) { - this._clearSingleClickTimeout() - - this._singleClickTimeout = setTimeout( - this._fireSingleClick.bind(this, e), - (this.options.singleClickTimeout || 500) - ) - }, - - _fireSingleClick: function (e) { - if (!e.originalEvent._stopped) { - this.fire('singleclick', L.Util.extend(e, { type: 'singleclick' })) - } - }, - - _clearSingleClickTimeout: function () { - if (this._singleClickTimeout !== null) { - clearTimeout(this._singleClickTimeout) - this._singleClickTimeout = null - } - } -}) - -class BaseMap extends Component { - static propTypes = { - config: PropTypes.object, - mapClick: PropTypes.func, - setLocation: PropTypes.func, // TODO: rename from action name to avoid namespace conflict? - toggleName: PropTypes.element - } - - /* Internal Methods */ - - _setLocationFromPopup = (type) => { - const { setMapPopupLocation, setLocation, popupLocation: location } = this.props - setMapPopupLocation({ location: null }) - setLocation({ type, location, reverseGeocode: true }) - if (typeof this.props.onSetLocation === 'function') { - this.props.onSetLocation({type, location}) - } - } - - _onClickTo = () => this._setLocationFromPopup('to') - - _onClickFrom = () => this._setLocationFromPopup('from') - - _onLeftClick = (e) => { - this.props.setMapPopupLocationAndGeocode({ location: constructLocation(e.latlng) }) - if (typeof this.props.onClick === 'function') this.props.onClick(e) - } - - _onOverlayAdd = ({ name }) => this.props.updateOverlayVisibility({ [name]: true }) - - _onOverlayRemove = ({ name }) => this.props.updateOverlayVisibility({ [name]: false }) - - // TODO: make map controlled component - _mapBoundsChanged = e => { - const bounds = e.target.getBounds() - if (!bounds.equals(this.props.mapState.bounds)) { - this.props.updateMapState({ bounds }) - } - } - - _onViewportChanged = ({ zoom }) => this.props.setMapZoom({ zoom }) - - _updateBounds (oldProps, newProps) { - // TODO: maybe setting bounds ought to be handled in map props... - - oldProps = oldProps || {} - newProps = newProps || {} - - // Don't auto-fit if popup us active - if (oldProps.popupLocation || newProps.popupLocation) return - - const { map } = this.refs - if (!map) return - - const padding = [30, 30] - - // Fit map to to entire itinerary if active itinerary bounds changed - const oldItinBounds = oldProps.itinerary && getItineraryBounds(oldProps.itinerary) - const fromChanged = !isEqual(oldProps.query && oldProps.query.from, newProps.query && newProps.query.from) - const toChanged = !isEqual(oldProps.query && oldProps.query.to, newProps.query && newProps.query.to) - const newItinBounds = newProps.itinerary && getItineraryBounds(newProps.itinerary) - if ( - (!oldItinBounds && newItinBounds) || - (oldItinBounds && newItinBounds && !oldItinBounds.equals(newItinBounds)) - ) { - map.leafletElement.fitBounds(newItinBounds, { padding }) - - // Pan to to itinerary leg if made active (clicked); newly active leg must be non-null - } else if (newProps.itinerary && newProps.activeLeg !== oldProps.activeLeg && newProps.activeLeg !== null) { - map.leafletElement.fitBounds( - getLegBounds(newProps.itinerary.legs[newProps.activeLeg]), - { padding } - ) - - // If no itinerary update but from/to locations are present, fit to those - } else if (newProps.query.from && newProps.query.to && (fromChanged || toChanged)) { - // On certain mobile devices (e.g., Android + Chrome), setting from and to - // locations via the location search component causes issues for this - // fitBounds invocation. The map does not appear to be visible when these - // prop changes are detected, so for now we should perhaps just skip this - // fitBounds on mobile. - // See https://github.com/opentripplanner/otp-react-redux/issues/133 for - // more info. - // TODO: Fix this so mobile devices will also update the bounds to the - // from/to locations. - if (!isMobile()) { - map.leafletElement.fitBounds([ - [newProps.query.from.lat, newProps.query.from.lon], - [newProps.query.to.lat, newProps.query.to.lon] - ], { padding }) - } - - // If only from or to is set, pan to that - } else if (newProps.query.from && fromChanged) { - map.leafletElement.panTo([newProps.query.from.lat, newProps.query.from.lon]) - } else if (newProps.query.to && toChanged) { - map.leafletElement.panTo([newProps.query.to.lat, newProps.query.to.lon]) - - // Pan to to itinerary step if made active (clicked) - } else if ( - newProps.itinerary && - newProps.activeLeg !== null && - newProps.activeStep !== null && - newProps.activeStep !== oldProps.activeStep - ) { - const leg = newProps.itinerary.legs[newProps.activeLeg] - const step = leg.steps[newProps.activeStep] - map.leafletElement.panTo([step.lat, step.lon]) - } - } - - _popupClosed = () => this.props.setMapPopupLocation({ location: null }) - - /** - * Checks whether the modes have changed between old and new queries and - * whether to update the map overlays accordingly (e.g., to show rental vehicle - * options on the map). - */ - _handleQueryChange = (oldQuery, newQuery) => { - const { overlays } = this.props - if (overlays && oldQuery.mode) { - // Determine any added/removed modes - const oldModes = oldQuery.mode.split(',') - const newModes = newQuery.mode.split(',') - const removed = oldModes.filter(m => !newModes.includes(m)) - const added = newModes.filter(m => !oldModes.includes(m)) - const overlayVisibility = {} - for (const oConfig of overlays) { - if (!oConfig.modes || oConfig.modes.length !== 1) continue - // TODO: support multi-mode overlays - const overlayMode = oConfig.modes[0] - - if ( - ( - overlayMode === 'CAR_RENT' || - overlayMode === 'CAR_HAIL' || - overlayMode === 'MICROMOBILITY_RENT' - ) && - oConfig.companies - ) { - // Special handling for company-based mode overlays (e.g. carshare, car-hail) - const overlayCompany = oConfig.companies[0] // TODO: handle multi-company overlays - if (added.includes(overlayMode)) { - // Company-based mode was just selected; enable overlay iff overlay's company is active - if (newQuery.companies.includes(overlayCompany)) overlayVisibility[oConfig.name] = true - } else if (removed.includes(overlayMode)) { - // Company-based mode was just deselected; disable overlay (regardless of company) - overlayVisibility[oConfig.name] = false - } else if (newModes.includes(overlayMode) && oldQuery.companies !== newQuery.companies) { - // Company-based mode remains selected but companies change - overlayVisibility[oConfig.name] = newQuery.companies.includes(overlayCompany) - } - } else { // Default handling for other modes - if (added.includes(overlayMode)) overlayVisibility[oConfig.name] = true - if (removed.includes(overlayMode)) overlayVisibility[oConfig.name] = false - } - } - // Only trigger update action if there are overlays to update. - if (Object.keys(overlayVisibility).length > 0) { - this.props.updateOverlayVisibility(overlayVisibility) - } - } - } - - /* React Lifecycle methods */ - - componentDidMount () { - this._updateBounds(null, this.props) - - const lmap = this.refs.map.leafletElement - lmap.options.singleClickTimeout = 250 - lmap.on('singleclick', (e) => { this._onLeftClick(e) }) - } - - componentDidUpdate (prevProps) { - this._updateBounds(prevProps, this.props) - // Check if any overlays should be toggled due to mode change - this._handleQueryChange(prevProps.query, this.props.query) - } - - // remove custom overlays on unmount - // TODO: Is this needed? It may have something to do with mobile vs desktop views - componentWillUnmount () { - const lmap = this.refs.map.leafletElement - lmap.eachLayer((layer) => { - lmap.removeLayer(layer) - }) - } - - render () { - const { config, children, diagramLeg, elevationPoint, popupLocation } = this.props - const { baseLayers } = config.map - const showElevationProfile = Boolean(config.elevationProfile) - // Separate overlay layers into user-controlled (those with a checkbox in - // the layer control) and those that are needed by the app (e.g., stop viewer - // and itinerary overlay). - const userControlledOverlays = [] - const fixedOverlays = [] - React.Children - .toArray(children) - .forEach(child => { - if (child.props.name) userControlledOverlays.push(child) - else fixedOverlays.push(child) - }) - - const center = config.map && config.map.initLat && config.map.initLon - ? [config.map.initLat, config.map.initLon] - : null - - // Compute the elevation point marker, if activeLeg and elevation profile is enabled. - let elevationPointMarker = null - if (showElevationProfile && diagramLeg && elevationPoint) { - const pos = legLocationAtDistance(diagramLeg, elevationPoint) - if (pos) { - elevationPointMarker = ( - - ) - } - } - - return ( - - {/* Create the layers control, including base map layers and any - * user-controlled overlays. */} - - {/* base layers */ - baseLayers && baseLayers.map((layer, i) => { - // If layer supports retina tiles, set tileSize and zoomOffset - // (see https://stackoverflow.com/a/37043490/915811). - // Otherwise, use detectRetina to request more tiles (scaled down): - // https://leafletjs.com/reference-1.6.0.html#tilelayer-detectretina - const retinaProps = layer.hasRetinaSupport - ? { tileSize: 512, zoomOffset: -1 } - : { detectRetina: true } - // If Browser doesn't support retina, remove @2x from URL so that - // retina tiles are not requested. - const url = L.Browser.retina - ? layer.url - : layer.url.replace('@2x', '') - return ( - - - - ) - }) - } - - {/* user-controlled overlay layers (e.g., vehicle locations, stops) */ - userControlledOverlays.map((child, i) => { - return ( - - {child} - - ) - }) - } - - - {/* Add the fixed, i.e. non-user-controllable, overlays (e.g., itinerary overlay) */} - {fixedOverlays} - - {/* Add the location selection popup, if visible */} - {popupLocation && ( - -
-
- {popupLocation.name.split(',').length > 3 - ? popupLocation.name.split(',').splice(0, 3).join(',') - : popupLocation.name - } -
-
- Plan a trip: - - {' '}|{' '} - - -
-
-
- )} - - {/* Add the elevation point marker */} - {elevationPointMarker} -
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - const overlays = state.otp.config.map && state.otp.config.map.overlays - ? state.otp.config.map.overlays - : [] - return { - activeLeg: activeSearch && activeSearch.activeLeg, - activeStep: activeSearch && activeSearch.activeStep, - config: state.otp.config, - diagramLeg: state.otp.ui.diagramLeg, - elevationPoint: state.otp.ui.elevationPoint, - mapState: state.otp.mapState, - isFromSet: - state.otp.currentQuery.from && - state.otp.currentQuery.from.lat !== null && - state.otp.currentQuery.from.lon !== null, - isToSet: - state.otp.currentQuery.to && - state.otp.currentQuery.to.lat !== null && - state.otp.currentQuery.to.lon !== null, - itinerary: getActiveItinerary(state.otp), - overlays, - popupLocation: state.otp.ui.mapPopupLocation, - query: state.otp.currentQuery - } -} - -const mapDispatchToProps = { - setLocation, - setMapPopupLocation, - setMapPopupLocationAndGeocode, - setMapZoom, - updateOverlayVisibility -} - -export default connect(mapStateToProps, mapDispatchToProps)(BaseMap) diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js new file mode 100644 index 000000000..6472f62df --- /dev/null +++ b/lib/components/map/bounds-updating-overlay.js @@ -0,0 +1,127 @@ +import isEqual from 'lodash.isequal' +import coreUtils from '@opentripplanner/core-utils' +import { MapLayer, withLeaflet } from 'react-leaflet' +import { connect } from 'react-redux' + +import { + getLeafletItineraryBounds, + getLeafletLegBounds +} from '../../util/itinerary' +import { getActiveItinerary, getActiveSearch } from '../../util/state' + +/** + * This MapLayer component will automatically update the leaflet bounds + * depending on what data is in the redux store. This component does not + * "render" anything on the map. + */ +class BoundsUpdatingOverlay extends MapLayer { + createLeafletElement () {} + + updateLeafletElement () {} + + componentDidMount () { + this.updateBounds(null, this.props) + } + + componentDidUpdate (prevProps) { + this.updateBounds(prevProps, this.props) + } + + componentWillUnmount () {} + + /* eslint-disable-next-line complexity */ + updateBounds (oldProps, newProps) { + // TODO: maybe setting bounds ought to be handled in map props... + + oldProps = oldProps || {} + newProps = newProps || {} + + // Don't auto-fit if popup us active + if (oldProps.popupLocation || newProps.popupLocation) return + + const { map } = newProps.leaflet + if (!map) return + + const padding = [30, 30] + + // Fit map to to entire itinerary if active itinerary bounds changed + const newFrom = newProps.query && newProps.query.from + const newItinBounds = newProps.itinerary && getLeafletItineraryBounds(newProps.itinerary) + const newTo = newProps.query && newProps.query.to + const oldFrom = oldProps.query && oldProps.query.from + const oldItinBounds = oldProps.itinerary && getLeafletItineraryBounds(oldProps.itinerary) + const oldTo = oldProps.query && oldProps.query.to + const fromChanged = !isEqual(oldFrom, newFrom) + const toChanged = !isEqual(oldTo, newTo) + if ( + (!oldItinBounds && newItinBounds) || + (oldItinBounds && newItinBounds && !oldItinBounds.equals(newItinBounds)) + ) { + map.fitBounds(newItinBounds, { padding }) + // Pan to to itinerary leg if made active (clicked); newly active leg must be non-null + } else if ( + newProps.itinerary && + newProps.activeLeg !== oldProps.activeLeg && + newProps.activeLeg !== null + ) { + map.fitBounds( + getLeafletLegBounds(newProps.itinerary.legs[newProps.activeLeg]), + { padding } + ) + + // If no itinerary update but from/to locations are present, fit to those + } else if (newFrom && newTo && (fromChanged || toChanged)) { + // On certain mobile devices (e.g., Android + Chrome), setting from and to + // locations via the location search component causes issues for this + // fitBounds invocation. The map does not appear to be visible when these + // prop changes are detected, so for now we should perhaps just skip this + // fitBounds on mobile. + // See https://github.com/opentripplanner/otp-react-redux/issues/133 for + // more info. + // TODO: Fix this so mobile devices will also update the bounds to the + // from/to locations. + if (!coreUtils.ui.isMobile()) { + map.fitBounds([ + [newFrom.lat, newFrom.lon], + [newTo.lat, newTo.lon] + ], { padding }) + } + + // If only from or to is set, pan to that + } else if (newFrom && fromChanged) { + map.panTo([newFrom.lat, newFrom.lon]) + } else if (newTo && toChanged) { + map.panTo([newTo.lat, newTo.lon]) + + // Pan to to itinerary step if made active (clicked) + } else if ( + newProps.itinerary && + newProps.activeLeg !== null && + newProps.activeStep !== null && + newProps.activeStep !== oldProps.activeStep + ) { + const leg = newProps.itinerary.legs[newProps.activeLeg] + const step = leg.steps[newProps.activeStep] + map.panTo([step.lat, step.lon]) + } + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const activeSearch = getActiveSearch(state.otp) + return { + activeLeg: activeSearch && activeSearch.activeLeg, + activeStep: activeSearch && activeSearch.activeStep, + itinerary: getActiveItinerary(state.otp), + popupLocation: state.otp.ui.mapPopupLocation, + query: state.otp.currentQuery + } +} + +const mapDispatchToProps = {} + +export default withLeaflet( + connect(mapStateToProps, mapDispatchToProps)(BoundsUpdatingOverlay) +) diff --git a/lib/components/map/endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js similarity index 51% rename from lib/components/map/endpoints-overlay.js rename to lib/components/map/connected-endpoints-overlay.js index 5967034b8..c609ce12d 100644 --- a/lib/components/map/endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -1,35 +1,14 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' +import EndpointsOverlay from '@opentripplanner/endpoints-overlay' import { connect } from 'react-redux' -import Endpoint from './endpoint' -import { clearLocation, forgetPlace, rememberPlace, setLocation } from '../../actions/map' +import { + clearLocation, + forgetPlace, + rememberPlace, + setLocation +} from '../../actions/map' import { getActiveSearch, getShowUserSettings } from '../../util/state' -class EndpointsOverlay extends Component { - static propTypes = { - query: PropTypes.object - } - render () { - const { query, showUserSettings } = this.props - const { from, to } = query - return ( -
- - -
- ) - } -} - // connect to the redux store const mapStateToProps = (state, ownProps) => { @@ -38,10 +17,13 @@ const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) const query = activeSearch ? activeSearch.query : state.otp.currentQuery const showUserSettings = getShowUserSettings(state.otp) + const { from, to } = query return { + fromLocation: from, locations: state.otp.user.locations, - query, - showUserSettings + showUserSettings, + toLocation: to, + visible: true } } diff --git a/lib/components/map/connected-park-and-ride-overlay.js b/lib/components/map/connected-park-and-ride-overlay.js new file mode 100644 index 000000000..8ac66fae2 --- /dev/null +++ b/lib/components/map/connected-park-and-ride-overlay.js @@ -0,0 +1,40 @@ +import ParkAndRideOverlay from '@opentripplanner/park-and-ride-overlay' +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { setLocation } from '../../actions/map' +import { parkAndRideQuery } from '../../actions/api' + +class ConnectedParkAndRideOverlay extends Component { + componentDidMount () { + const params = {} + if (this.props.maxTransitDistance) { + params.maxTransitDistance = this.props.maxTransitDistance + } + // TODO: support config-defined bounding envelope + + this.props.parkAndRideQuery(params) + } + + render () { + return ( + + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + parkAndRideLocations: state.otp.overlay.parkAndRide && + state.otp.overlay.parkAndRide.locations + } +} + +const mapDispatchToProps = { + setLocation, + parkAndRideQuery +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConnectedParkAndRideOverlay) diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js new file mode 100644 index 000000000..1aa83fce2 --- /dev/null +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -0,0 +1,17 @@ +import RouteViewerOverlay from '@opentripplanner/route-viewer-overlay' +import { connect } from 'react-redux' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const viewedRoute = state.otp.ui.viewedRoute + return { + routeData: viewedRoute && state.otp.transitIndex.routes + ? state.otp.transitIndex.routes[viewedRoute.routeId] + : null + } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(RouteViewerOverlay) diff --git a/lib/components/map/connected-stop-marker.js b/lib/components/map/connected-stop-marker.js new file mode 100644 index 000000000..4ae8fbdd9 --- /dev/null +++ b/lib/components/map/connected-stop-marker.js @@ -0,0 +1,20 @@ +import DefaultStopMarker from '@opentripplanner/stops-overlay/lib/stop-marker' +import { connect } from 'react-redux' + +import { setLocation } from '../../actions/map' +import { setViewedStop } from '../../actions/ui' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + languageConfig: state.otp.config.language + } +} + +const mapDispatchToProps = { + setLocation, + setViewedStop +} + +export default connect(mapStateToProps, mapDispatchToProps)(DefaultStopMarker) diff --git a/lib/components/map/connected-stop-viewer-overlay.js b/lib/components/map/connected-stop-viewer-overlay.js new file mode 100644 index 000000000..ead3f0868 --- /dev/null +++ b/lib/components/map/connected-stop-viewer-overlay.js @@ -0,0 +1,19 @@ +import StopViewerOverlay from '@opentripplanner/stop-viewer-overlay' +import DefaultStopMarker from '@opentripplanner/stop-viewer-overlay/lib/default-stop-marker' +import { connect } from 'react-redux' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const viewedStop = state.otp.ui.viewedStop + return { + stopData: viewedStop + ? state.otp.transitIndex.stops[viewedStop.stopId] + : null, + StopMarker: DefaultStopMarker + } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(StopViewerOverlay) diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js new file mode 100644 index 000000000..ff77d1a07 --- /dev/null +++ b/lib/components/map/connected-stops-overlay.js @@ -0,0 +1,20 @@ +import StopsOverlay from '@opentripplanner/stops-overlay' +import StopMarker from './connected-stop-marker' +import { connect } from 'react-redux' + +import { findStopsWithinBBox } from '../../actions/api' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + StopMarker, + stops: state.otp.overlay.transit.stops + } +} + +const mapDispatchToProps = { + refreshStops: findStopsWithinBBox +} + +export default connect(mapStateToProps, mapDispatchToProps)(StopsOverlay) diff --git a/lib/components/map/connected-transitive-overlay.js b/lib/components/map/connected-transitive-overlay.js new file mode 100644 index 000000000..aa7843fd2 --- /dev/null +++ b/lib/components/map/connected-transitive-overlay.js @@ -0,0 +1,39 @@ +import coreUtils from '@opentripplanner/core-utils' +import TransitiveCanvasOverlay from '@opentripplanner/transitive-overlay' +import { connect } from 'react-redux' + +import { getActiveSearch, getActiveItineraries } from '../../util/state' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const activeSearch = getActiveSearch(state.otp) + let transitiveData = null + if ( + activeSearch && + activeSearch.query.routingType === 'ITINERARY' && + activeSearch.response && + activeSearch.response.plan + ) { + const itins = getActiveItineraries(state.otp) + // TODO: prevent itineraryToTransitive() from being called more than needed + transitiveData = coreUtils.map.itineraryToTransitive(itins[activeSearch.activeItinerary]) + } else if ( + activeSearch && + activeSearch.response && + activeSearch.response.otp + ) { + transitiveData = activeSearch.response.otp + } + + return { + activeItinerary: activeSearch && activeSearch.activeItinerary, + routingType: activeSearch && activeSearch.query && activeSearch.query.routingType, + transitiveData, + visible: true + } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(TransitiveCanvasOverlay) diff --git a/lib/components/map/connected-trip-viewer-overlay.js b/lib/components/map/connected-trip-viewer-overlay.js new file mode 100644 index 000000000..f0c0c6325 --- /dev/null +++ b/lib/components/map/connected-trip-viewer-overlay.js @@ -0,0 +1,17 @@ +import TripViewerOverlay from '@opentripplanner/trip-viewer-overlay' +import { connect } from 'react-redux' + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const viewedTrip = state.otp.ui.viewedTrip + return { + tripData: viewedTrip + ? state.otp.transitIndex.trips[viewedTrip.tripId] + : null + } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(TripViewerOverlay) diff --git a/lib/components/map/connected-vehicle-rental-overlay.js b/lib/components/map/connected-vehicle-rental-overlay.js new file mode 100644 index 000000000..c98f01b88 --- /dev/null +++ b/lib/components/map/connected-vehicle-rental-overlay.js @@ -0,0 +1,45 @@ +import VehicleRentalOverlay from '@opentripplanner/vehicle-rental-overlay' +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { setLocation } from '../../actions/map' + +class ConnectedVehicleRentalOverlay extends Component { + constructor (props) { + super(props) + this.state = { visible: props.visible } + } + + componentDidMount () { + this.props.registerOverlay(this) + } + + onOverlayAdded = () => { + this.setState({ visible: true }) + } + + onOverlayRemoved = () => { + this.setState({ visible: false }) + } + + render () { + return ( + + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + configCompanies: state.otp.config.companies, + zoom: state.otp.config.map.initZoom + } +} + +const mapDispatchToProps = { + setLocation +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConnectedVehicleRentalOverlay) diff --git a/lib/components/map/current-position-marker.js b/lib/components/map/current-position-marker.js deleted file mode 100644 index b7c6348e7..000000000 --- a/lib/components/map/current-position-marker.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, CircleMarker } from 'react-leaflet' - -/* A small circular marker showing the user's current position. Intended - * primarily for use in mobile mode. - */ - -class CurrentPositionMarker extends MapLayer { - static propTypes = { - currentPosition: PropTypes.object - } - - // TODO: determine why the default MapLayer componentWillUnmount() method throws an error - componentWillUnmount () { } - componentDidMount () { } - - createLeafletElement () { - } - - updateLeafletElement () { - } - - render () { - const { currentPosition } = this.props - - if (!currentPosition || !currentPosition.coords) return - - return ( - - - - ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - currentPosition: state.otp.location.currentPosition - } -} - -export default connect(mapStateToProps)(CurrentPositionMarker) diff --git a/lib/components/map/default-map.js b/lib/components/map/default-map.js index b9432ffce..32e63111b 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -1,24 +1,61 @@ +import BaseMap from '@opentripplanner/base-map' import React, { Component } from 'react' import { connect } from 'react-redux' +import styled from 'styled-components' import { bikeRentalQuery, carRentalQuery, vehicleRentalQuery } from '../../actions/api' -import BaseMap from './base-map' -import EndpointsOverlay from './endpoints-overlay' -import ParkAndRideOverlay from './park-and-ride-overlay' -import StopsOverlay from './stops-overlay' -import StopViewerOverlay from './stop-viewer-overlay' +import { + setLocation, + setMapPopupLocation, + setMapPopupLocationAndGeocode +} from '../../actions/map' +import BoundsUpdatingOverlay from './bounds-updating-overlay' +import EndpointsOverlay from './connected-endpoints-overlay' +import ParkAndRideOverlay from './connected-park-and-ride-overlay' +import RouteViewerOverlay from './connected-route-viewer-overlay' +import StopViewerOverlay from './connected-stop-viewer-overlay' +import StopsOverlay from './connected-stops-overlay' +import TransitiveOverlay from './connected-transitive-overlay' +import TripViewerOverlay from './connected-trip-viewer-overlay' +import VehicleRentalOverlay from './connected-vehicle-rental-overlay' +import ElevationPointMarker from './elevation-point-marker' +import PointPopup from './point-popup' import TileOverlay from './tile-overlay' -import TransitiveOverlay from './transitive-overlay' -import TripViewerOverlay from './trip-viewer-overlay' -import RouteViewerOverlay from './route-viewer-overlay' -import VehicleRentalOverlay from './vehicle-rental-overlay' import ZipcarOverlay from './zipcar-overlay' +const MapContainer = styled.div` + height: 100%; + width: 100%; + + .map { + height: 100%; + width: 100%; + } + + * { + box-sizing: unset; + } +` + class DefaultMap extends Component { + onMapClick = (e) => { + this.props.setMapPopupLocationAndGeocode(e) + } + + onPopupClosed = () => { + this.props.setMapPopupLocation({ location: null }) + } + + onSetLocationFromPopup = (payload) => { + const { setLocation, setMapPopupLocation } = this.props + setMapPopupLocation({ location: null }) + setLocation(payload) + } + render () { const { bikeRentalQuery, @@ -26,58 +63,82 @@ class DefaultMap extends Component { carRentalQuery, carRentalStations, mapConfig, + mapPopupLocation, vehicleRentalQuery, vehicleRentalStations } = this.props + + const center = mapConfig && mapConfig.initLat && mapConfig.initLon + ? [mapConfig.initLat, mapConfig.initLon] + : null + + const popup = mapPopupLocation && { + contents: ( + + ), + location: [mapPopupLocation.lat, mapPopupLocation.lon] + } + return ( - Map View} - {...this.props} - > - {/* The default overlays */} - - - - - - - {/* The configurable overlays */} - {mapConfig.overlays && mapConfig.overlays.map((overlayConfig, k) => { - switch (overlayConfig.type) { - case 'bike-rental': return ( - - ) - case 'car-rental': return ( - - ) - case 'park-and-ride': return - case 'stops': return - case 'tile': return - case 'micromobility-rental': return ( - - ) - case 'zipcar': return - default: return null - } - })} - + + + {/* The default overlays */} + + + + + + + + + {/* The configurable overlays */} + {mapConfig.overlays && mapConfig.overlays.map((overlayConfig, k) => { + switch (overlayConfig.type) { + case 'bike-rental': return ( + + ) + case 'car-rental': return ( + + ) + case 'park-and-ride': + return + case 'stops': return + case 'tile': return + case 'micromobility-rental': return ( + + ) + case 'zipcar': return + default: return null + } + })} + + ) } } @@ -89,6 +150,7 @@ const mapStateToProps = (state, ownProps) => { bikeRentalStations: state.otp.overlay.bikeRental.stations, carRentalStations: state.otp.overlay.carRental.stations, mapConfig: state.otp.config.map, + mapPopupLocation: state.otp.ui.mapPopupLocation, vehicleRentalStations: state.otp.overlay.vehicleRental.stations } } @@ -96,6 +158,9 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { bikeRentalQuery, carRentalQuery, + setLocation, + setMapPopupLocation, + setMapPopupLocationAndGeocode, vehicleRentalQuery } diff --git a/lib/components/map/distance-measure.js b/lib/components/map/distance-measure.js deleted file mode 100644 index 0d331b27d..000000000 --- a/lib/components/map/distance-measure.js +++ /dev/null @@ -1,21 +0,0 @@ -import { MapControl, withLeaflet } from 'react-leaflet' -import L from 'leaflet' -import 'leaflet.polylinemeasure/Leaflet.PolylineMeasure.js' - -class DistanceMeasure extends MapControl { - createLeafletElement (props) { - return L.control.polylineMeasure({ - unit: 'landmiles', - measureControlLabel: '📏', - backgroundColor: '#f3dd2d', - clearMeasurementsOnStop: true - }) - } - - componentDidMount () { - const { map } = this.props.leaflet - this.leafletElement.addTo(map) - } -} - -export default withLeaflet(DistanceMeasure) diff --git a/lib/components/map/elevation-point-marker.js b/lib/components/map/elevation-point-marker.js new file mode 100644 index 000000000..5013b1a8e --- /dev/null +++ b/lib/components/map/elevation-point-marker.js @@ -0,0 +1,53 @@ +import coreUtils from '@opentripplanner/core-utils' +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { CircleMarker } from 'react-leaflet' + +/** + * As the OTP user moves the cursor over the elevation tracking chart + * of a walking or biking leg (to see which point of their itinerary is at which elevation), + * ElevationPointMarker displays and moves a marker on the map to highlight + * the location that corresponds to the cursor position on the elevation chart, + * so the user can see the streets and paths that correspond to a portion of an elevation profile. + */ +class ElevationPointMarker extends Component { + render () { + const { diagramLeg, elevationPoint, showElevationProfile } = this.props + + // Compute the elevation point marker, if activeLeg and elevation profile is enabled. + let elevationPointMarker = null + if (showElevationProfile && diagramLeg && elevationPoint) { + const pos = coreUtils.itinerary.legLocationAtDistance( + diagramLeg, + elevationPoint + ) + if (pos) { + elevationPointMarker = ( + + ) + } + } + return elevationPointMarker + } +} + +const mapStateToProps = (state, ownProps) => { + return { + diagramLeg: state.otp.ui.diagramLeg, + elevationPoint: state.otp.ui.elevationPoint, + showElevationProfile: !!state.otp.config.elevationProfile + } +} + +const mapDispatchToProps = {} + +export default connect(mapStateToProps, mapDispatchToProps)(ElevationPointMarker) diff --git a/lib/components/map/endpoint.js b/lib/components/map/endpoint.js deleted file mode 100644 index 3d625e912..000000000 --- a/lib/components/map/endpoint.js +++ /dev/null @@ -1,150 +0,0 @@ -import React, { Component } from 'react' -import ReactDOMServer from 'react-dom/server' - -import { Button } from 'react-bootstrap' -import { Marker, Popup } from 'react-leaflet' -import { divIcon } from 'leaflet' - -import Icon from '../narrative/icon' -import { constructLocation, matchLatLon } from '../../util/map' -import LocationIcon from '../icons/location-icon' - -export default class Endpoint extends Component { - _rememberAsHome = () => { - const { rememberPlace } = this.props - const location = Object.assign({}, this.props.location) - location.id = 'home' - location.icon = 'home' - location.type = 'home' - rememberPlace({ type: 'home', location }) - } - - _rememberAsWork = () => { - const { rememberPlace } = this.props - const location = Object.assign({}, this.props.location) - location.id = 'work' - location.icon = 'briefcase' - location.type = 'work' - rememberPlace({ type: 'work', location }) - } - - _forgetHome = () => this.props.forgetPlace('home') - - _forgetWork = () => this.props.forgetPlace('work') - - _clearLocation = () => { - const { clearLocation, type } = this.props - clearLocation({ type }) - } - - _swapLocation = () => { - const { location, setLocation, type } = this.props - this._clearLocation() - const otherType = type === 'from' ? 'to' : 'from' - setLocation({ type: otherType, location }) - } - - _onDragEnd = (e) => { - const { setLocation, type } = this.props - const location = constructLocation(e.target.getLatLng()) - setLocation({ type, location, reverseGeocode: true }) - } - - render () { - const { type, location, locations, showPopup } = this.props - const position = location && location.lat && location.lon ? [location.lat, location.lon] : null - if (!position) return null - const fgStyle = { fontSize: 24, width: 32, height: 32 } - const bgStyle = { fontSize: 32, width: 32, height: 32, paddingTop: 1 } - const match = locations.find(l => matchLatLon(l, location)) - const isWork = match && match.type === 'work' - const isHome = match && match.type === 'home' - const iconHtml = ReactDOMServer.renderToStaticMarkup( - - {type === 'from' - // From icon should have white circle background - ? - : - - - - } - - - ) - const otherType = type === 'from' ? 'to' : 'from' - const icon = isWork - ? 'briefcase' - : isHome - ? 'home' - : 'map-marker' - return ( - - {showPopup && - -
- - {location.name} - -
- -
-
- -
-
- -
-
- -
-
-
- } -
- ) - } -} diff --git a/lib/components/map/itinerary-legs.js b/lib/components/map/itinerary-legs.js deleted file mode 100644 index f43e18a99..000000000 --- a/lib/components/map/itinerary-legs.js +++ /dev/null @@ -1,47 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import { FeatureGroup, GeoJSON } from 'react-leaflet' -import polyline from '@mapbox/polyline' - -import { isTransit } from '../../util/itinerary' - -export default class ItineraryLegs extends Component { - static propTypes = { - itinerary: PropTypes.object, - activeLeg: PropTypes.number, - setActiveLeg: PropTypes.func - } - _onLegClick = (e) => { - const index = e.layer.feature.geometry.index - const leg = this.props.itinerary.legs[index] - if (index === this.props.activeLeg) { - this.props.setActiveLeg(null) - } else { - this.props.setActiveLeg(index, leg) - } - } - render () { - const { itinerary, activeLeg } = this.props - return ( - - {itinerary.legs.map((leg, index) => { - const geojson = polyline.toGeoJSON(leg.legGeometry.points) - geojson.index = index - const active = activeLeg === index - const color = active - ? 'yellow' - : isTransit(leg.mode) - ? 'blue' - : 'black' - return ( - - ) - })} - - ) - } -} diff --git a/lib/components/map/itinerary-overlay.js b/lib/components/map/itinerary-overlay.js deleted file mode 100644 index 55f47b130..000000000 --- a/lib/components/map/itinerary-overlay.js +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' - -import { - setActiveLeg -} from '../../actions/narrative' -import { getActiveItinerary, getActiveSearch } from '../../util/state' -import ItinerarySteps from './itinerary-steps' -import ItineraryStops from './itinerary-stops' -import ItineraryLegs from './itinerary-legs' - -class ItineraryOverlay extends Component { - static propTypes = { - activeLeg: PropTypes.number, - activeStep: PropTypes.number, - itinerary: PropTypes.object - } - - render () { - const { activeLeg, activeStep, itinerary } = this.props - if (!itinerary) return null - return ( -
- - - -
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - return { - itinerary: getActiveItinerary(state.otp), - activeLeg: activeSearch && activeSearch.activeLeg, - activeStep: activeSearch && activeSearch.activeStep - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - setActiveLeg: (index, leg) => { dispatch(setActiveLeg({ index, leg })) } - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(ItineraryOverlay) diff --git a/lib/components/map/itinerary-steps.js b/lib/components/map/itinerary-steps.js deleted file mode 100644 index 4f4bea8d8..000000000 --- a/lib/components/map/itinerary-steps.js +++ /dev/null @@ -1,53 +0,0 @@ -import { divIcon } from 'leaflet' -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { Marker } from 'react-leaflet' - -import { getStepInstructions } from '../../util/itinerary' - -export default class ItinerarySteps extends Component { - static propTypes = { - itinerary: PropTypes.object - } - addItineraryStop (array, item) { - if (item.stopId && array.indexOf(item.stopId) === -1) { - array.push(item) - } - } - render () { - const { itinerary, activeLeg, activeStep } = this.props - let steps = [] - itinerary.legs.map((l, legIndex) => { - steps = [ - ...steps, - ...l.steps.map((s, stepIndex) => { - s.legIndex = legIndex - s.stepIndex = stepIndex - return s - }) - ] - }) - return ( -
- {steps.map((step, index) => { - if (step.relativeDirection === 'DEPART') { - return null - } - const active = step.legIndex === activeLeg && step.stepIndex === activeStep - const icon = divIcon({ - html: ``, - className: '' - }) - return ( - - ) - })} -
- ) - } -} diff --git a/lib/components/map/itinerary-stops.js b/lib/components/map/itinerary-stops.js deleted file mode 100644 index f1b669145..000000000 --- a/lib/components/map/itinerary-stops.js +++ /dev/null @@ -1,46 +0,0 @@ -import { divIcon } from 'leaflet' -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { Marker } from 'react-leaflet' -// import polyline from '@mapbox/polyline' - -// import { isTransit } from '../../util/itinerary' - -export default class ItineraryStops extends Component { - static propTypes = { - itinerary: PropTypes.object - } - addItineraryStop (array, item) { - if (item.stopId && array.indexOf(item.stopId) === -1) { - array.push(item) - } - } - render () { - const { itinerary } = this.props - const stops = [] - itinerary.legs.map(l => { - this.addItineraryStop(stops, l.from) - this.addItineraryStop(stops, l.to) - }) - return ( -
- {stops.map((stop, index) => { - const icon = divIcon({ - html: ` - - - `, - className: '' - }) - return ( - - ) - })} -
- ) - } -} diff --git a/lib/components/map/leaflet-canvas-layer.js b/lib/components/map/leaflet-canvas-layer.js deleted file mode 100644 index 0fd96ae38..000000000 --- a/lib/components/map/leaflet-canvas-layer.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - Generic Canvas Layer for leaflet 0.7 and 1.0-rc, - copyright Stanislav Sumbera, 2016 , sumbera.com , license MIT - originally created and motivated by L.CanvasOverlay available here: https://gist.github.com/Sumbera/11114288 - -*/ - -/* eslint-disable */ - -// -- L.DomUtil.setTransform from leaflet 1.0.0 to work on 0.0.7 -//------------------------------------------------------------------------------ -L.DomUtil.setTransform = L.DomUtil.setTransform || function (el, offset, scale) { - var pos = offset || new L.Point(0, 0); - - el.style[L.DomUtil.TRANSFORM] = - (L.Browser.ie3d ? - 'translate(' + pos.x + 'px,' + pos.y + 'px)' : - 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + - (scale ? ' scale(' + scale + ')' : ''); -}; - -// -- support for both 0.0.7 and 1.0.0 rc2 leaflet -L.CanvasLayer = (L.Layer ? L.Layer : L.Class).extend({ - // -- initialized is called on prototype - initialize: function (options) { - this._map = null; - this._canvas = null; - this._frame = null; - this._delegate = null; - L.setOptions(this, options); - }, - - delegate :function(del){ - this._delegate = del; - return this; - }, - - needRedraw: function () { - if (!this._frame) { - this._frame = L.Util.requestAnimFrame(this.drawLayer, this); - } - return this; - }, - - //------------------------------------------------------------- - _onLayerDidResize: function (resizeEvent) { - this._canvas.width = resizeEvent.newSize.x; - this._canvas.height = resizeEvent.newSize.y; - }, - //------------------------------------------------------------- - _onLayerDidMove: function () { - var topLeft = this._map.containerPointToLayerPoint([0, 0]); - L.DomUtil.setPosition(this._canvas, topLeft); - this.drawLayer(); - }, - //------------------------------------------------------------- - getEvents: function () { - var events = { - resize: this._onLayerDidResize, - moveend: this._onLayerDidMove, - zoom: this._onLayerDidMove - }; - if (this._map.options.zoomAnimation && L.Browser.any3d) { - events.zoomanim = this._animateZoom; - } - - return events; - }, - //------------------------------------------------------------- - onAdd: function (map) { - this._map = map; - this._canvas = L.DomUtil.create('canvas', 'leaflet-layer'); - this.tiles = {}; - - var size = this._map.getSize(); - this._canvas.width = size.x; - this._canvas.height = size.y; - - var animated = this._map.options.zoomAnimation && L.Browser.any3d; - L.DomUtil.addClass(this._canvas, 'leaflet-zoom-' + (animated ? 'animated' : 'hide')); - - - map._panes.overlayPane.appendChild(this._canvas); - - map.on(this.getEvents(),this); - - var del = this._delegate || this; - del.onLayerDidMount && del.onLayerDidMount(); // -- callback - this.needRedraw(); - }, - - //------------------------------------------------------------- - onRemove: function (map) { - var del = this._delegate || this; - del.onLayerWillUnmount && del.onLayerWillUnmount(); // -- callback - - - map.getPanes().overlayPane.removeChild(this._canvas); - - map.off(this.getEvents(),this); - - this._canvas = null; - - }, - - //------------------------------------------------------------ - addTo: function (map) { - map.addLayer(this); - return this; - }, - // -------------------------------------------------------------------------------- - LatLonToMercator: function (latlon) { - return { - x: latlon.lng * 6378137 * Math.PI / 180, - y: Math.log(Math.tan((90 + latlon.lat) * Math.PI / 360)) * 6378137 - }; - }, - - //------------------------------------------------------------------------------ - drawLayer: function () { - // -- todo make the viewInfo properties flat objects. - var size = this._map.getSize(); - var bounds = this._map.getBounds(); - var zoom = this._map.getZoom(); - - var center = this.LatLonToMercator(this._map.getCenter()); - var corner = this.LatLonToMercator(this._map.containerPointToLatLng(this._map.getSize())); - - var del = this._delegate || this; - del.onDrawLayer && del.onDrawLayer( { - layer : this, - canvas: this._canvas, - bounds: bounds, - size: size, - zoom: zoom, - center : center, - corner : corner - }); - this._frame = null; - }, - // -- L.DomUtil.setTransform from leaflet 1.0.0 to work on 0.0.7 - //------------------------------------------------------------------------------ - _setTransform: function (el, offset, scale) { - var pos = offset || new L.Point(0, 0); - - el.style[L.DomUtil.TRANSFORM] = - (L.Browser.ie3d ? - 'translate(' + pos.x + 'px,' + pos.y + 'px)' : - 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + - (scale ? ' scale(' + scale + ')' : ''); - }, - - //------------------------------------------------------------------------------ - _animateZoom: function (e) { - var scale = this._map.getZoomScale(e.zoom); - // -- different calc of animation zoom in leaflet 1.0.3 thanks @peterkarabinovic, @jduggan1 - var offset = L.Layer ? this._map._latLngBoundsToNewLayerBounds(this._map.getBounds(), e.zoom, e.center).min : - this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos()); - - L.DomUtil.setTransform(this._canvas, offset, scale); - - - } -}); - -L.canvasLayer = function () { - return new L.CanvasLayer(); -}; diff --git a/lib/components/map/leg-diagram.js b/lib/components/map/leg-diagram.js index 901c49608..917d1f3bf 100644 --- a/lib/components/map/leg-diagram.js +++ b/lib/components/map/leg-diagram.js @@ -1,12 +1,14 @@ import memoize from 'lodash.memoize' -import React, {Component} from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, {Component} from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import ReactResizeDetector from 'react-resize-detector' import { setElevationPoint, showLegDiagram } from '../../actions/map' -import { getElevationProfile, getTextWidth, legElevationAtDistance } from '../../util/itinerary' + +const { getElevationProfile, getTextWidth, legElevationAtDistance } = coreUtils.itinerary // Fixed dimensions for chart const height = 160 diff --git a/lib/components/map/park-and-ride-overlay.js b/lib/components/map/park-and-ride-overlay.js deleted file mode 100644 index 3cbc66d5e..000000000 --- a/lib/components/map/park-and-ride-overlay.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, Marker, Popup, withLeaflet } from 'react-leaflet' -import { divIcon } from 'leaflet' - -import SetFromToButtons from './set-from-to' -import { setLocation } from '../../actions/map' -import { parkAndRideQuery } from '../../actions/api' - -class ParkAndRideOverlay extends MapLayer { - static propTypes = { - locations: PropTypes.array, - zipcarLocationsQuery: PropTypes.func, - setLocation: PropTypes.func - } - - componentDidMount () { - const params = {} - if (this.props.maxTransitDistance) { - params['maxTransitDistance'] = this.props.maxTransitDistance - } - // TODO: support config-defined bounding envelope - - this.props.parkAndRideQuery(params) - } - - componentWillUnmount () {} - - createLeafletElement () {} - - updateLeafletElement () {} - - render () { - const { locations } = this.props - if (!locations || locations.length === 0) return - - const markerIcon = divIcon({ - iconSize: [20, 20], - popupAnchor: [0, -10], - html: '
P
', - className: '' - }) - - return ( - - {locations.map((location, k) => { - const name = location.name.startsWith('P+R ') ? location.name.substring(4) : location.name - return ( - - -
- {/* Popup title */} -
- {name} -
- - {/* Set as from/to toolbar */} -
- -
-
-
-
- ) - })} -
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - locations: state.otp.overlay.parkAndRide && state.otp.overlay.parkAndRide.locations - } -} - -const mapDispatchToProps = { - setLocation, - parkAndRideQuery -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(ParkAndRideOverlay)) diff --git a/lib/components/map/point-popup.js b/lib/components/map/point-popup.js new file mode 100644 index 000000000..b49fa567e --- /dev/null +++ b/lib/components/map/point-popup.js @@ -0,0 +1,34 @@ +import FromToLocationPicker from '@opentripplanner/from-to-location-picker' +import styled from 'styled-components' + +const PopupContainer = styled.div` + width: 240px; +` + +const PopupTitle = styled.div` + font-size: 14px; + margin-bottom: 6px; +` + +export default function MapPopup ({ + mapPopupLocation, + onSetLocationFromPopup +}) { + return ( + + + {mapPopupLocation.name.split(',').length > 3 + ? mapPopupLocation.name.split(',').splice(0, 3).join(',') + : mapPopupLocation.name + } + +
+ Plan a trip: + +
+
+ ) +} diff --git a/lib/components/map/route-viewer-overlay.js b/lib/components/map/route-viewer-overlay.js deleted file mode 100644 index 129c7d25c..000000000 --- a/lib/components/map/route-viewer-overlay.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, Polyline, withLeaflet } from 'react-leaflet' - -import polyline from '@mapbox/polyline' - -// helper fn to check if geometry has been populated for all patterns in route -const isGeomComplete = routeData => { - return ( - routeData && - routeData.patterns && - Object.values(routeData.patterns) - .every(ptn => typeof ptn.geometry !== 'undefined') - ) -} - -class RouteViewerOverlay extends MapLayer { - static propTypes = {} - - componentDidMount () {} - - // TODO: determine why the default MapLayer componentWillUnmount() method throws an error - componentWillUnmount () {} - - componentDidUpdate (prevProps) { - // if pattern geometry just finished populating, update the map points - if ( - !isGeomComplete(prevProps.routeData) && - isGeomComplete(this.props.routeData) - ) { - const allPoints = Object.values(this.props.routeData.patterns).reduce( - (acc, ptn) => { - return acc.concat(polyline.decode(ptn.geometry.points)) - }, - [] - ) - this.props.leaflet.map.fitBounds(allPoints) - } - } - - createLeafletElement () {} - - updateLeafletElement () {} - - render () { - const { routeData } = this.props - - if (!routeData || !routeData.patterns) return - - const routeColor = routeData.color ? `#${routeData.color}` : '#00bfff' - const segments = [] - Object.values(routeData.patterns).forEach(pattern => { - if (!pattern.geometry) return - const pts = polyline.decode(pattern.geometry.points) - segments.push( - - ) - }) - - return segments.length > 0 - ?
{segments}
- : - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const viewedRoute = state.otp.ui.viewedRoute - return { - viewedRoute, - routeData: viewedRoute && state.otp.transitIndex.routes - ? state.otp.transitIndex.routes[viewedRoute.routeId] - : null - } -} - -const mapDispatchToProps = {} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(RouteViewerOverlay)) diff --git a/lib/components/map/set-from-to.js b/lib/components/map/set-from-to.js index 600771faa..80d383bb6 100644 --- a/lib/components/map/set-from-to.js +++ b/lib/components/map/set-from-to.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' -import { Button } from 'react-bootstrap' -import LocationIcon from '../icons/location-icon' + +import FromToLocationPicker from '@opentripplanner/from-to-location-picker' export default class SetFromToButtons extends Component { _setLocation = (type) => { @@ -18,19 +18,7 @@ export default class SetFromToButtons extends Component { render () { return ( -
- Plan a trip: - - -
+ ) } } diff --git a/lib/components/map/stop-viewer-overlay.js b/lib/components/map/stop-viewer-overlay.js deleted file mode 100644 index 790ded83b..000000000 --- a/lib/components/map/stop-viewer-overlay.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, Popup, CircleMarker, withLeaflet } from 'react-leaflet' - -class StopViewerOverlay extends MapLayer { - static propTypes = { - stopData: PropTypes.object, - viewedStop: PropTypes.object - } - - componentDidMount () { } - - // TODO: determine why the default MapLayer componentWillUnmount() method throws an error - componentWillUnmount () { } - - /** - * Only reset map view if a new stop is selected. This prevents resetting the - * bounds if, for example, the arrival times have changed for the same stop - * in the viewer. - */ - componentDidUpdate (prevProps) { - const nextStop = this.props.stopData - const oldStopId = prevProps.stopData && prevProps.stopData.id - const hasNewStopId = nextStop && nextStop.id !== oldStopId - if (hasNewStopId) this.props.leaflet.map.setView([nextStop.lat, nextStop.lon]) - } - - createLeafletElement () { } - - updateLeafletElement () { } - - render () { - const { viewedStop, stopData } = this.props - - if (!viewedStop || !stopData) return - - return ( - - - -
- {stopData.name} -
-
-
-
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const viewedStop = state.otp.ui.viewedStop - return { - viewedStop: viewedStop, - stopData: viewedStop - ? state.otp.transitIndex.stops[viewedStop.stopId] - : null - } -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(StopViewerOverlay)) diff --git a/lib/components/map/stops-overlay.js b/lib/components/map/stops-overlay.js deleted file mode 100644 index 3e178c6b1..000000000 --- a/lib/components/map/stops-overlay.js +++ /dev/null @@ -1,178 +0,0 @@ -import { divIcon } from 'leaflet' -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, Popup, Marker, withLeaflet } from 'react-leaflet' -import { Button } from 'react-bootstrap' - -import SetFromToButtons from './set-from-to' -import { isMobile } from '../../util/ui' -import { findStopsWithinBBox, clearStops } from '../../actions/api' -import { setLocation } from '../../actions/map' -import { setViewedStop, setMainPanelContent } from '../../actions/ui' - -class StopsOverlay extends MapLayer { - static propTypes = { - minZoom: PropTypes.number, - queryMode: PropTypes.string, - stops: PropTypes.array, - refreshStops: PropTypes.func - } - - static defaultProps = { - minZoom: 15 - } - - componentDidMount () { - // set up pan/zoom listener - this.props.leaflet.map.on('moveend', () => { - this._refreshStops() - }) - } - - // TODO: determine why the default MapLayer componentWillUnmount() method throws an error - componentWillUnmount () { } - - _refreshStops () { - if (this.props.leaflet.map.getZoom() < this.props.minZoom) { - this.forceUpdate() - return - } - - const bounds = this.props.leaflet.map.getBounds() - if (!bounds.equals(this.lastBounds)) { - setTimeout(() => { - this.props.refreshStops({ - minLat: bounds.getSouth(), - maxLat: bounds.getNorth(), - minLon: bounds.getWest(), - maxLon: bounds.getEast() - }) - this.lastBounds = bounds - }, 300) - } - } - - createLeafletElement () { - } - - updateLeafletElement () { - } - - render () { - const { leaflet, minZoom, setLocation, setViewedStop, setMainPanelContent, stops, languageConfig } = this.props - const mobileView = isMobile() - - // Don't render if below zoom threshold or no stops visible - if (this.props.leaflet.map.getZoom() < minZoom || !stops || stops.length === 0) { - return - } - - // Helper to create StopMarker from stop - const createStopMarker = (stop) => - - // Singleton case; return FeatureGroup with single StopMarker - if (stops.length === 1) { - return {createStopMarker(stops[0])} - } - - // Otherwise, return FeatureGroup with mapped array of StopMarkers - return {stops.map(stop => createStopMarker(stop))} - } -} - -class StopMarker extends Component { - static propTypes = { - mobileView: PropTypes.bool, - setLocation: PropTypes.func, - setViewedStop: PropTypes.func, - setMainPanelContent: PropTypes.func, - stop: PropTypes.object - } - - _onClickView = () => { - this.props.setMainPanelContent(null) - this.props.setViewedStop({ stopId: this.props.stop.id }) - } - - render () { - const { setLocation, stop, languageConfig } = this.props - const { id, name, lat, lon } = stop - const idArr = id.split(':') - const radius = 20 - const half = radius / 2 - const quarter = radius / 4 - const html = `
` - const icon = divIcon({ - html, - className: 'stop-overlay-bg', - iconSize: radius - }) - - return ( - - -
-
{name}
- -
Agency: {idArr[0]}
-
- Stop ID: {idArr[1]} - {/* The Stop Viewer button - * Note: we use a vanilla Button instead of ViewStopButton because - * connected components don't work within react-leaflet Popups) - * TODO: Make ViewStopButton work here, perhaps w/ React 16 portals - */} - -
- - {/* The "Set as [from/to]" ButtonGroup */} -
- -
-
-
-
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - stops: state.otp.overlay.transit.stops, - queryMode: state.otp.currentQuery.mode, - languageConfig: state.otp.config.language - } -} - -const mapDispatchToProps = { - refreshStops: findStopsWithinBBox, - clearStops, - setLocation, - setViewedStop, - setMainPanelContent -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(StopsOverlay)) diff --git a/lib/components/map/stylized-map.js b/lib/components/map/stylized-map.js index cf4a124fa..54e25509c 100644 --- a/lib/components/map/stylized-map.js +++ b/lib/components/map/stylized-map.js @@ -1,13 +1,12 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' import { select, event } from 'd3-selection' import { zoom } from 'd3-zoom' - +import coreUtils from '@opentripplanner/core-utils' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { connect } from 'react-redux' import Transitive from 'transitive-js' import { getActiveSearch, getActiveItineraries } from '../../util/state' -import { isBikeshareStation, itineraryToTransitive } from '../../util/map' var STYLES = {} @@ -16,7 +15,7 @@ STYLES.places = { if ( place.getId() !== 'from' && place.getId() !== 'to' && - !isBikeshareStation(place) + !coreUtils.map.isBikeshareStation(place) ) { return 'none' } @@ -117,7 +116,7 @@ const mapStateToProps = (state, ownProps) => { activeSearch.response.plan ) { const itins = getActiveItineraries(state.otp) - transitiveData = itineraryToTransitive(itins[activeSearch.activeItinerary]) + transitiveData = coreUtils.map.itineraryToTransitive(itins[activeSearch.activeItinerary]) } else if ( activeSearch && activeSearch.response && diff --git a/lib/components/map/transitive-overlay.js b/lib/components/map/transitive-overlay.js deleted file mode 100644 index 6ca8258c1..000000000 --- a/lib/components/map/transitive-overlay.js +++ /dev/null @@ -1,171 +0,0 @@ -import PropTypes from 'prop-types' -import { MapLayer, withLeaflet } from 'react-leaflet' -import L from 'leaflet' -import { connect } from 'react-redux' -import Transitive from 'transitive-js' -import isEqual from 'lodash.isequal' - -import { getActiveSearch, getActiveItineraries } from '../../util/state' -import { itineraryToTransitive } from '../../util/map' - -require('./leaflet-canvas-layer') - -// TODO: move to util? -function checkHiPPI (canvas) { - if (window.devicePixelRatio > 1) { - const PIXEL_RATIO = 2 - canvas.style.width = canvas.width + 'px' - canvas.style.height = canvas.height + 'px' - - canvas.width *= PIXEL_RATIO - canvas.height *= PIXEL_RATIO - - var context = canvas.getContext('2d') - context.scale(PIXEL_RATIO, PIXEL_RATIO) - } -} - -const zoomFactors = [{ - minScale: 0, - gridCellSize: 0, - internalVertexFactor: 0, - angleConstraint: 5, - mergeVertexThreshold: 0, - useGeographicRendering: true -}] - -class TransitiveCanvasOverlay extends MapLayer { - static propTypes = { - transitiveData: PropTypes.object - } - - // React Lifecycle Methods - - componentDidMount () { - const { map } = this.props.leaflet - L.canvasLayer() - .delegate(this) // -- if we do not inherit from L.CanvasLayer we can setup a delegate to receive events from L.CanvasLayer - .addTo(map) - } - - componentDidUpdate (prevProps) { - // Check if we received new transitive data - if (this._transitive && !isEqual(prevProps.transitiveData, this.props.transitiveData)) { - this._transitive.updateData(this.props.transitiveData) - if (!this.props.transitiveData) this._transitive.render() - else this._updateBoundsAndRender() - } - - if ( // this block only applies for profile trips where active option changed - this.props.routingType === 'PROFILE' && - prevProps.activeItinerary !== this.props.activeItinerary - ) { - if (this.props.activeItinerary == null) { // no option selected; clear focus - this._transitive.focusJourney(null) - this._transitive.render() - } else if (this.props.transitiveData) { - this._transitive.focusJourney(this.props.transitiveData.journeys[this.props.activeItinerary].journey_id) - this._transitive.render() - } - } - } - - componentWillUnmount () { - if (this._transitive) { - this._transitive.updateData(null) - this._transitive.render() - } - } - - // Internal Methods - - _initTransitive (canvas) { - const { map } = this.props.leaflet - - // set up the transitive instance - const mapBounds = map.getBounds() - this._transitive = new Transitive({ - data: this.props.transitiveData, - initialBounds: [[mapBounds.getWest(), mapBounds.getSouth()], [mapBounds.getEast(), mapBounds.getNorth()]], - zoomEnabled: false, - autoResize: false, - styles: require('./transitive-styles'), - zoomFactors, - display: 'canvas', - canvas - }) - - checkHiPPI(canvas) - - // the initial map draw - this._updateBoundsAndRender() - } - - _updateBoundsAndRender () { - if (!this._transitive) { - console.log('WARNING: Transitive object not set in transitive-canvas-overlay') - return - } - - const mapBounds = this.props.leaflet.map.getBounds() - this._transitive.setDisplayBounds([[mapBounds.getWest(), mapBounds.getSouth()], [mapBounds.getEast(), mapBounds.getNorth()]]) - this._transitive.render() - } - - // Leaflet Layer API Methods - - onDrawLayer (info) { - if (!this._transitive) this._initTransitive(info.canvas) - - const mapSize = this.props.leaflet.map.getSize() - if ( - this._lastMapSize && ( - mapSize.x !== this._lastMapSize.x || - mapSize.y !== this._lastMapSize.y - ) - ) { - const canvas = info.canvas - checkHiPPI(canvas) - this._transitive.display.setDimensions(mapSize.x, mapSize.y) - this._transitive.display.setCanvas(canvas) - } - - this._updateBoundsAndRender() - - this._lastMapSize = this.props.leaflet.map.getSize() - } - - createTile (coords) { - } - - createLeafletElement (props) { - } - - updateLeafletElement (fromProps, toProps) { - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - let transitiveData = null - if (activeSearch && activeSearch.query.routingType === 'ITINERARY' && activeSearch.response && activeSearch.response.plan) { - const itins = getActiveItineraries(state.otp) - // TODO: prevent itineraryToTransitive() from being called more than needed - transitiveData = itineraryToTransitive(itins[activeSearch.activeItinerary]) - } else if (activeSearch && activeSearch.response && activeSearch.response.otp) { - transitiveData = activeSearch.response.otp - } - - return { - transitiveData, - activeItinerary: activeSearch && activeSearch.activeItinerary, - routingType: activeSearch && activeSearch.query && activeSearch.query.routingType - } -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(TransitiveCanvasOverlay)) diff --git a/lib/components/map/transitive-styles.js b/lib/components/map/transitive-styles.js deleted file mode 100644 index 73621c583..000000000 --- a/lib/components/map/transitive-styles.js +++ /dev/null @@ -1,124 +0,0 @@ -import { isBikeshareStation } from '../../util/map' - -var STYLES = {} - -/* STYLES.segments = { - - // override the default stroke color - stroke: (display, segment) => { - if (!segment.focused) return - - switch (segment.type) { - case 'CAR': - return '#888' - case 'WALK': - return '#86cdf9' - case 'BICYCLE': - case 'BICYCLE_RENT': - return '#f00' - } - }, - - // override the default stroke width - 'stroke-width': (display, segment, index, utils) => { - switch (segment.type) { - case 'CAR': - return utils.pixels(display.zoom.scale(), 2, 4, 6) + 'px' - case 'WALK': - return '5px' - case 'BICYCLE': - case 'BICYCLE_RENT': - return '4px' - case 'TRANSIT': - // bus segments: - if (segment.mode === 3) { - return utils.pixels(display.zoom.scale(), 2, 4, 8) + 'px' - } - // all others: - return utils.pixels(display.zoom.scale(), 4, 8, 12) + 'px' - } - }, - - // specify the dash-array - 'stroke-dasharray': (display, segment) => { - switch (segment.type) { - case 'CAR': - return '3,2' - case 'WALK': - return '0.01,7' - case 'BICYCLE': - case 'BICYCLE_RENT': - return '6,3' - } - }, - - // specify the line cap type - 'stroke-linecap': (display, segment) => { - switch (segment.type) { - case 'WALK': - return 'round' - case 'CAR': - case 'BICYCLE': - case 'BICYCLE_RENT': - return 'butt' - } - } -} */ - -/** style overrides for places (i.e. the start and end icons) **/ - -STYLES.places = { - display: function (display, place) { - if (!isBikeshareStation(place)) return 'none' - }, - fill: '#f00', - stroke: '#fff', - 'stroke-width': 2, - r: 7 -} - -/* function getIconSize (data) { - return isBikeshareStation(data.owner) ? 14 : 30 -} - -STYLES.places_icon = { - display: function (display, data) { - if (!isBikeshareStation(data.owner)) return 'none' - }, - - // center the icon by offsetting by half the width/height - x: function (display, data) { - return -getIconSize(data) / 2 - }, - y: function (display, data) { - return -getIconSize(data) / 2 - }, - width: function (display, data) { - return getIconSize(data) - }, - height: function (display, data) { - return getIconSize(data) - }, - 'xlink:href': function (display, data) { - const place = data.owner - if (place.getId() === 'from') return `data:image/svg+xml;utf8,${fromSvg}` - if (place.getId() === 'to') return `data:image/svg+xml;utf8,${toSvg}` - if (isBikeshareStation(place)) return `data:image/svg+xml;utf8,${bikeSvg}` - }, - stroke: 0 -} */ - -/* STYLES.segments_halo = { - 'stroke-width': function (display, data, index, utils) { - return data.computeLineWidth(display) + 7 - }, - opacity: 0.75 -} */ - -STYLES.stops_merged = { - r: function (display, data, index, utils) { - return 6 - } -} - -export default STYLES diff --git a/lib/components/map/trip-viewer-overlay.js b/lib/components/map/trip-viewer-overlay.js deleted file mode 100644 index 42368a392..000000000 --- a/lib/components/map/trip-viewer-overlay.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { FeatureGroup, MapLayer, Polyline, withLeaflet } from 'react-leaflet' - -import polyline from '@mapbox/polyline' - -class TripViewerOverlay extends MapLayer { - static propTypes = { - tripData: PropTypes.object, - viewedTrip: PropTypes.object - } - - componentDidMount () { } - - // TODO: determine why the default MapLayer componentWillUnmount() method throws an error - componentWillUnmount () { } - - componentDidUpdate (prevProps) { - const oldGeometry = prevProps.tripData && prevProps.tripData.geometry - const newGeometry = this.props.tripData && this.props.tripData.geometry - if (oldGeometry === newGeometry || !newGeometry) return - const pts = polyline.decode(newGeometry.points) - this.props.leaflet.map.fitBounds(pts) - } - - createLeafletElement () { } - - updateLeafletElement () { } - - render () { - const { tripData } = this.props - - if (!tripData || !tripData.geometry) return - - const pts = polyline.decode(tripData.geometry.points) - return ( - - - - ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - const viewedTrip = state.otp.ui.viewedTrip - return { - viewedTrip, - tripData: viewedTrip - ? state.otp.transitIndex.trips[viewedTrip.tripId] - : null - } -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(TripViewerOverlay)) diff --git a/lib/components/map/vehicle-rental-overlay.js b/lib/components/map/vehicle-rental-overlay.js deleted file mode 100644 index 89d82dc2a..000000000 --- a/lib/components/map/vehicle-rental-overlay.js +++ /dev/null @@ -1,247 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { CircleMarker, FeatureGroup, Marker, MapLayer, Popup, withLeaflet } from 'react-leaflet' -import { divIcon } from 'leaflet' - -import { setLocation } from '../../actions/map' -import SetFromToButtons from './set-from-to' -import { getCompaniesLabelFromNetworks } from '../../util/itinerary' - -class VehicleRentalOverlay extends MapLayer { - static propTypes = { - queryMode: PropTypes.string, - vehicles: PropTypes.array, - refreshVehicles: PropTypes.func - } - - createLeafletElement () { } - - updateLeafletElement () { } - - _startRefreshing () { - // ititial station retrieval - this.props.refreshVehicles() - - // set up timer to refresh stations periodically - this._refreshTimer = setInterval(() => { - this.props.refreshVehicles() - }, 30000) // defaults to every 30 sec. TODO: make this configurable?*/ - } - - _stopRefreshing () { - if (this._refreshTimer) clearInterval(this._refreshTimer) - } - - componentDidMount () { - const {companies, mapSymbols, name, visible} = this.props - if (visible) this._startRefreshing() - if (!mapSymbols) console.warn(`No map symbols provided for layer ${name}`, companies) - } - - componentWillUnmount () { - this._stopRefreshing() - } - - componentDidUpdate (prevProps) { - if (!prevProps.visible && this.props.visible) { - this._startRefreshing() - } else if (prevProps.visible && !this.props.visible) { - this._stopRefreshing() - } - } - - /** - * Render some popup html for a station. This contains custom logic for - * displaying rental vehicles in the TriMet MOD website that might not be - * applicable to other regions. - */ - _renderPopupForStation = (station, stationIsHub = false) => { - const {configCompanies, leaflet, setLocation} = this.props - const stationNetworks = getCompaniesLabelFromNetworks( - station.networks, - configCompanies - ) - let stationName = station.name || station.id - if (station.isFloatingBike) { - stationName = `Free-floating bike: ${stationName}` - } else if (station.isFloatingCar) { - stationName = `${stationNetworks} ${stationName}` - } else if (station.isFloatingVehicle) { - // assumes that all floating vehicles are E-scooters - stationName = `${stationNetworks} E-scooter` - } else { - stationIsHub = true - } - return ( - -
- {/* Popup title */} -
{stationName}
- - {/* render dock info if it is available */} - {stationIsHub && ( -
-
Available bikes: {station.bikesAvailable}
-
Available docks: {station.spacesAvailable}
-
- )} - - {/* Set as from/to toolbar */} -
- -
-
-
- ) - } - - _renderStationAsCircle = (station, symbolDef) => { - let strokeColor = symbolDef.strokeColor || symbolDef.fillColor - if (!station.isFloatingBike) { - strokeColor = symbolDef.dockStrokeColor || strokeColor - } - return ( - - {this._renderPopupForStation(station)} - - ) - } - - _renderStationAsHubAndFloatingBike = (station) => { - let icon - if (station.isFloatingBike) { - icon = divIcon({ - iconSize: [24, 24], - iconAnchor: [12, 24], - popupAnchor: [0, -12], - html: `
`, - className: '' - }) - } else { - const pctFull = station.bikesAvailable / (station.bikesAvailable + station.spacesAvailable) - const i = Math.round(pctFull * 9) - icon = divIcon({ - iconSize: [24, 24], - iconAnchor: [12, 24], - popupAnchor: [0, -12], - html: `
`, - className: '' - }) - } - return ( - - {this._renderPopupForStation(station, !station.isFloatingBike)} - - ) - } - - _renderStationAsMarker = (station, symbolDef) => { - const {baseIconClass} = this.props - let classes = `fa fa-map-marker ${baseIconClass}` - // If this station is exclusive to a single network, apply the the class for that network - if (station.networks.length === 1) { - classes += ` ${baseIconClass}-${station.networks[0].toLowerCase()}` - } - const color = symbolDef && symbolDef.fillColor - ? symbolDef.fillColor - : 'gray' - const markerIcon = divIcon({ - className: '', - iconSize: [11, 16], - popupAnchor: [0, -6], - html: `` - }) - - return ( - - {this._renderPopupForStation(station)} - - ) - } - - _renderStation = (station) => { - // render the station according to any map symbol configuration - const {mapSymbols} = this.props - - // no config set, just render a default marker - if (!mapSymbols) return this._renderStationAsMarker(station) - - // get zoom to check which symbol to render - const zoom = this.props.leaflet.map.getZoom() - - for (let i = 0; i < mapSymbols.length; i++) { - const symbolDef = mapSymbols[i] - if (symbolDef.minZoom <= zoom && symbolDef.maxZoom >= zoom) { - switch (symbolDef.type) { - case 'circle': - return this._renderStationAsCircle(station, symbolDef) - case 'hubAndFloatingBike': - return this._renderStationAsHubAndFloatingBike(station) - default: - return this._renderStationAsMarker(station, symbolDef) - } - } - } - - // no matching symbol definition, render default marker - return this._renderStationAsMarker(station) - } - - render () { - const { stations, companies } = this.props - let filteredStations = stations - if (companies) { - filteredStations = stations.filter( - station => station.networks.filter(value => companies.includes(value)).length > 0 - ) - } - - if (!filteredStations || filteredStations.length === 0) return - - return ( - - {filteredStations.map(this._renderStation)} - - ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - configCompanies: state.otp.config.companies, - zoom: state.otp.config.map.initZoom - } -} - -const mapDispatchToProps = { - setLocation -} - -export default connect(mapStateToProps, mapDispatchToProps)(withLeaflet(VehicleRentalOverlay)) diff --git a/lib/components/map/zipcar-overlay.js b/lib/components/map/zipcar-overlay.js index a007a3120..399831bfb 100644 --- a/lib/components/map/zipcar-overlay.js +++ b/lib/components/map/zipcar-overlay.js @@ -33,7 +33,15 @@ class ZipcarOverlay extends MapLayer { } componentDidMount () { - if (this.props.visible) this._startRefreshing() + this.props.registerOverlay(this) + } + + onOverlayAdded = () => { + this._startRefreshing() + } + + onOverlayRemoved = () => { + this._stopRefreshing() } componentWillUnmount () { diff --git a/lib/components/mobile/location-search.js b/lib/components/mobile/location-search.js index 5c7404947..11a7e02d6 100644 --- a/lib/components/mobile/location-search.js +++ b/lib/components/mobile/location-search.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' -import LocationField from '../form/location-field' +import LocationField from '../form/connected-location-field' import { MobileScreens, setMobileScreen } from '../../actions/ui' @@ -36,12 +36,12 @@ class MobileLocationSearch extends Component { />
diff --git a/lib/components/mobile/main.js b/lib/components/mobile/main.js index 0dcd5d08a..abec5db02 100644 --- a/lib/components/mobile/main.js +++ b/lib/components/mobile/main.js @@ -18,8 +18,9 @@ import { getActiveItinerary } from '../../util/state' class MobileMain extends Component { static propTypes = { currentQuery: PropTypes.object, - icons: PropTypes.object, itineraryClass: PropTypes.func, + LegIcon: PropTypes.elementType.isRequired, + ModeIcon: PropTypes.elementType.isRequired, map: PropTypes.element, setMobileScreen: PropTypes.func, title: PropTypes.element, @@ -45,7 +46,7 @@ class MobileMain extends Component { } render () { - const { icons, itineraryClass, itineraryFooter, map, title, uiState } = this.props + const { itineraryClass, itineraryFooter, LegIcon, map, ModeIcon, title, uiState } = this.props // check for route viewer if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { @@ -73,7 +74,6 @@ class MobileMain extends Component { case MobileScreens.SEARCH_FORM: return ( @@ -99,11 +99,17 @@ class MobileMain extends Component { return case MobileScreens.SET_OPTIONS: - return + return case MobileScreens.RESULTS_SUMMARY: - return - + return ( + + ) default: return

Invalid mobile screen

} diff --git a/lib/components/mobile/options-screen.js b/lib/components/mobile/options-screen.js index 893ba1357..3b3b3fbf1 100644 --- a/lib/components/mobile/options-screen.js +++ b/lib/components/mobile/options-screen.js @@ -4,14 +4,14 @@ import { connect } from 'react-redux' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' -import SettingsSelectorPanel from '../form/settings-selector-panel' +import ConnectedSettingsSelectorPanel from '../form/connected-settings-selector-panel' import PlanTripButton from '../form/plan-trip-button' import { MobileScreens, setMobileScreen } from '../../actions/ui' class MobileOptionsScreen extends Component { static propTypes = { - icons: PropTypes.object + ModeIcon: PropTypes.elementType.isRequired } _planTripClicked = () => { @@ -19,7 +19,7 @@ class MobileOptionsScreen extends Component { } render () { - const { icons } = this.props + const { ModeIcon } = this.props return ( @@ -30,7 +30,7 @@ class MobileOptionsScreen extends Component { />
- +
diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 125713358..209bacca4 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -1,12 +1,14 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' +import LocationIcon from '@opentripplanner/location-icon' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Button, Col, Row } from 'react-bootstrap' +import { connect } from 'react-redux' +import styled from 'styled-components' import DefaultMap from '../map/default-map' import ErrorMessage from '../form/error-message' import ItineraryCarousel from '../narrative/itinerary-carousel' -import LocationIcon from '../icons/location-icon' import MobileContainer from './container' import MobileNavigationBar from './navigation-bar' @@ -15,7 +17,35 @@ import { MobileScreens, setMobileScreen } from '../../actions/ui' import { setUseRealtimeResponse } from '../../actions/narrative' import { clearActiveSearch } from '../../actions/form' import { getActiveSearch, getRealtimeEffects } from '../../util/state' -import { enableScrollForSelector } from '../../util/ui' + +const LocationContainer = styled.div` + font-weight: 300; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const LocationSummaryContainer = styled.div` + height: 50px; + left: 0; + padding-right: 10px; + position: fixed; + right: 0; + top: 50px; +` + +const LocationsSummaryColFromTo = styled(Col)` + font-size: 1.1em; + line-height: 1.2em; +` + +const LocationsSummaryRow = styled(Row)` + padding: 4px 8px; +` + +const StyledLocationIcon = styled(LocationIcon)` + margin: 3px; +` class MobileResultsScreen extends Component { static propTypes = { @@ -38,7 +68,7 @@ class MobileResultsScreen extends Component { // Get the target element that we want to persist scrolling for // FIXME Do we need to add something that removes the listeners when // component unmounts? - enableScrollForSelector('.mobile-narrative-container') + coreUtils.ui.enableScrollForSelector('.mobile-narrative-container') } componentDidUpdate (prevProps) { @@ -65,7 +95,17 @@ class MobileResultsScreen extends Component { _toggleRealtime = () => this.props.setUseRealtimeResponse({useRealtime: !this.props.useRealtime}) render () { - const { error, icons, itineraryClass, itineraryFooter, query, realtimeEffects, resultCount, useRealtime, activeItineraryIndex } = this.props + const { + activeItineraryIndex, + error, + itineraryClass, + itineraryFooter, + LegIcon, + query, + realtimeEffects, + resultCount, + useRealtime + } = this.props const { expanded } = this.state const narrativeContainerStyle = expanded @@ -95,24 +135,24 @@ class MobileResultsScreen extends Component { */ const locationsSummary = ( -
- - -
- { query.from ? query.from.name : '' } -
-
- { query.to ? query.to.name : '' } -
- + + + + + { query.from ? query.from.name : '' } + + + { query.to ? query.to.name : '' } + + -
-
+ + ) if (error) { @@ -175,7 +215,7 @@ class MobileResultsScreen extends Component { expanded={this.state.expanded} onClick={this._optionClicked} showRealtimeAnnotation={showRealtimeAnnotation} - customIcons={icons} + LegIcon={LegIcon} />
diff --git a/lib/components/mobile/search-screen.js b/lib/components/mobile/search-screen.js index 364b3d17f..b21a56693 100644 --- a/lib/components/mobile/search-screen.js +++ b/lib/components/mobile/search-screen.js @@ -5,7 +5,7 @@ import { Row, Col } from 'react-bootstrap' import DateTimePreview from '../form/date-time-preview' import DefaultMap from '../map/default-map' -import LocationField from '../form/location-field' +import LocationField from '../form/connected-location-field' import PlanTripButton from '../form/plan-trip-button' import SettingsPreview from '../form/settings-preview' import SwitchButton from '../form/switch-button' @@ -17,9 +17,7 @@ import { MobileScreens, setMobileScreen } from '../../actions/ui' class MobileSearchScreen extends Component { static propTypes = { - icons: PropTypes.object, map: PropTypes.element, - setMobileScreen: PropTypes.func } @@ -44,20 +42,18 @@ class MobileSearchScreen extends Component { } render () { - const { icons } = this.props - return (
@@ -75,7 +71,6 @@ class MobileSearchScreen extends Component { diff --git a/lib/components/mobile/welcome-screen.js b/lib/components/mobile/welcome-screen.js index d162605a3..d31712c05 100644 --- a/lib/components/mobile/welcome-screen.js +++ b/lib/components/mobile/welcome-screen.js @@ -3,14 +3,12 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import MobileContainer from './container' -import LocationField from '../form/location-field' -import UserSettings from '../form/user-settings' +import LocationField from '../form/connected-location-field' import DefaultMap from '../map/default-map' import MobileNavigationBar from './navigation-bar' import { MobileScreens, setMobileScreen } from '../../actions/ui' import { setLocationToCurrent } from '../../actions/map' -import { getShowUserSettings } from '../../util/state' class MobileWelcomeScreen extends Component { static propTypes = { @@ -33,26 +31,25 @@ class MobileWelcomeScreen extends Component { // endpoint to be the current user location. (If selected as the 'to' point, // no action is needed since 'from' is the current location by default.) if (selection.type === 'from') { - this.props.setLocationToCurrent({ type: 'to' }) + this.props.setLocationToCurrent({ locationType: 'to' }) } } render () { - const { showUserSettings, title } = this.props + const { title } = this.props return (
- {showUserSettings ? : null}
) @@ -62,8 +59,7 @@ class MobileWelcomeScreen extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const showUserSettings = getShowUserSettings(state.otp) - return { showUserSettings } + return {} } const mapDispatchToProps = { diff --git a/lib/components/narrative/connected-trip-details.js b/lib/components/narrative/connected-trip-details.js new file mode 100644 index 000000000..f094f559d --- /dev/null +++ b/lib/components/narrative/connected-trip-details.js @@ -0,0 +1,23 @@ +import coreUtils from '@opentripplanner/core-utils' +import TripDetailsBase from '@opentripplanner/trip-details' +import { connect } from 'react-redux' +import styled from 'styled-components' + +const TripDetails = styled(TripDetailsBase)` + b { + font-weight: 600; + } +` + +// Connect imported TripDetails class to redux store. + +const mapStateToProps = (state, ownProps) => { + return { + routingType: state.otp.currentQuery.routingType, + tnc: state.otp.tnc, + timeFormat: coreUtils.time.getTimeFormat(state.otp.config), + longDateFormat: coreUtils.time.getLongDateFormat(state.otp.config) + } +} + +export default connect(mapStateToProps)(TripDetails) diff --git a/lib/components/narrative/default/access-leg.js b/lib/components/narrative/default/access-leg.js index 274282544..1b541635a 100644 --- a/lib/components/narrative/default/access-leg.js +++ b/lib/components/narrative/default/access-leg.js @@ -1,12 +1,10 @@ -import React, {Component} from 'react' +import coreUtils from '@opentripplanner/core-utils' +import { humanizeDistanceString } from '@opentripplanner/humanize-distance' import PropTypes from 'prop-types' +import React, {Component} from 'react' import Icon from '../icon' - import LegDiagramPreview from '../leg-diagram-preview' -import { distanceString } from '../../../util/distance' -import { getStepInstructions } from '../../../util/itinerary' -import { formatDuration } from '../../../util/time' /** * Default access leg component for narrative itinerary. @@ -48,9 +46,9 @@ export default class AccessLeg extends Component { {leg.mode} {' '} - {formatDuration(leg.duration)} + {coreUtils.time.formatDuration(leg.duration)} {' '} - ({distanceString(leg.distance)}) + ({humanizeDistanceString(leg.distance)}) {active &&
@@ -62,8 +60,8 @@ export default class AccessLeg extends Component { key={stepIndex} className={`step ${stepIsActive ? 'active' : ''}`} onClick={(e) => this._onStepClick(e, step, stepIndex)}> - {distanceString(step.distance)} - {getStepInstructions(step)} + {humanizeDistanceString(step.distance)} + {coreUtils.itinerary.getStepInstructions(step)} ) })} diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index b6b66a227..fcbeca04a 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -1,11 +1,13 @@ +import coreUtils from '@opentripplanner/core-utils' import React from 'react' import NarrativeItinerary from '../narrative-itinerary' import ItinerarySummary from './itinerary-summary' import ItineraryDetails from './itinerary-details' -import TripDetails from '../trip-details' +import TripDetails from '../connected-trip-details' import TripTools from '../trip-tools' -import { formatDuration, formatTime } from '../../../util/time' + +const { formatDuration, formatTime } = coreUtils.time export default class DefaultItinerary extends NarrativeItinerary { render () { @@ -16,6 +18,7 @@ export default class DefaultItinerary extends NarrativeItinerary { expanded, index, itinerary, + LegIcon, setActiveLeg, setActiveStep } = this.props @@ -28,7 +31,7 @@ export default class DefaultItinerary extends NarrativeItinerary { Itinerary {index + 1}{' '} {formatDuration(itinerary.duration)}{' '} {formatTime(itinerary.startTime)}—{formatTime(itinerary.endTime)} - + {(active || expanded) &&
@@ -38,6 +41,7 @@ export default class DefaultItinerary extends NarrativeItinerary { activeStep={activeStep} setActiveLeg={setActiveLeg} setActiveStep={setActiveStep} + LegIcon={LegIcon} /> diff --git a/lib/components/narrative/default/itinerary-details.js b/lib/components/narrative/default/itinerary-details.js index 2eec48805..337943dcc 100644 --- a/lib/components/narrative/default/itinerary-details.js +++ b/lib/components/narrative/default/itinerary-details.js @@ -1,27 +1,29 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import AccessLeg from './access-leg' import TransitLeg from './transit-leg' -import { isTransit } from '../../../util/itinerary' export default class ItineraryDetails extends Component { static propTypes = { - itinerary: PropTypes.object + itinerary: PropTypes.object, + LegIcon: PropTypes.elementType.isRequired } render () { - const { itinerary, activeLeg, activeStep, setActiveLeg, setActiveStep } = this.props + const { itinerary, activeLeg, activeStep, LegIcon, setActiveLeg, setActiveStep } = this.props return (
{itinerary.legs.map((leg, index) => { const legIsActive = activeLeg === index - return isTransit(leg.mode) + return coreUtils.itinerary.isTransit(leg.mode) ? : { @@ -26,7 +25,7 @@ export default class ItinerarySummary extends Component { // Add the mode icon blocks.push(
- +
) diff --git a/lib/components/narrative/default/tnc-leg.js b/lib/components/narrative/default/tnc-leg.js index 0c8505ff5..8b2a301ef 100644 --- a/lib/components/narrative/default/tnc-leg.js +++ b/lib/components/narrative/default/tnc-leg.js @@ -1,15 +1,17 @@ import currencyFormatter from 'currency-formatter' -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { connect } from 'react-redux' import { getTransportationNetworkCompanyEtaEstimate, getTransportationNetworkCompanyRideEstimate } from '../../../actions/api' -import { toSentenceCase } from '../../../util/itinerary' -import { formatDuration } from '../../../util/time' -import { isMobile } from '../../../util/ui' + +const { toSentenceCase } = coreUtils.itinerary +const { formatDuration } = coreUtils.time +const { isMobile } = coreUtils.ui class TransportationNetworkCompanyLeg extends Component { static propTypes = { diff --git a/lib/components/narrative/default/transit-leg.js b/lib/components/narrative/default/transit-leg.js index 5809f3f1b..44b826ecd 100644 --- a/lib/components/narrative/default/transit-leg.js +++ b/lib/components/narrative/default/transit-leg.js @@ -1,17 +1,18 @@ -import Icon from '../icon' -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' -import ModeIcon from '../../icons/mode-icon' +import Icon from '../icon' import ViewTripButton from '../../viewers/view-trip-button' import ViewStopButton from '../../viewers/view-stop-button' -import { getMapColor } from '../../../util/itinerary' -import { formatDuration, formatTime } from '../../../util/time' +const { getMapColor } = coreUtils.itinerary +const { formatDuration, formatTime } = coreUtils.time export default class TransitLeg extends Component { static propTypes = { - itinerary: PropTypes.object + itinerary: PropTypes.object, + LegIcon: PropTypes.elementType.isRequired } constructor (props) { @@ -34,7 +35,7 @@ export default class TransitLeg extends Component { } render () { - const { active, index, leg } = this.props + const { active, index, leg, LegIcon } = this.props const { expanded } = this.state const numStops = leg.to.stopIndex - leg.from.stopIndex - 1 @@ -46,7 +47,7 @@ export default class TransitLeg extends Component { onClick={(e) => this._onLegClick(e, leg, index)} >
- +
diff --git a/lib/components/narrative/itinerary-carousel.js b/lib/components/narrative/itinerary-carousel.js index ce7a373fa..55ad57435 100644 --- a/lib/components/narrative/itinerary-carousel.js +++ b/lib/components/narrative/itinerary-carousel.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import SwipeableViews from 'react-swipeable-views' @@ -8,10 +9,7 @@ import { setActiveItinerary, setActiveLeg, setActiveStep } from '../../actions/n import Icon from './icon' import DefaultItinerary from './default/default-itinerary' import Loading from './loading' -import NarrativeProfileSummary from './narrative-profile-summary' import { getActiveItineraries, getActiveSearch } from '../../util/state' -import { profileOptionsToItineraries } from '../../util/profile' -import { getTimeFormat } from '../../util/time' class ItineraryCarousel extends Component { state = {} @@ -26,8 +24,6 @@ class ItineraryCarousel extends Component { setActiveLeg: PropTypes.func, setActiveStep: PropTypes.func, expanded: PropTypes.bool, - showProfileSummary: PropTypes.bool, - profileOptions: PropTypes.array, companies: PropTypes.string } @@ -54,18 +50,11 @@ class ItineraryCarousel extends Component { } render () { - const { activeItinerary, itineraries, itineraryClass, hideHeader, pending, showProfileSummary } = this.props + const { activeItinerary, itineraries, itineraryClass, hideHeader, pending } = this.props if (pending) return if (!itineraries) return null - let views = [] - if (showProfileSummary) { - views.push(
-
Your Best Options (Swipe to View All)
- -
) - } - views = views.concat(itineraries.map((itinerary, index) => { + const views = itineraries.map((itinerary, index) => { return React.createElement(itineraryClass, { itinerary, index, @@ -74,7 +63,7 @@ class ItineraryCarousel extends Component { onClick: this._onItineraryClick, ...this.props }) - })) + }) return (
@@ -112,28 +101,18 @@ class ItineraryCarousel extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) - let itineraries = null - let profileOptions = null - let showProfileSummary = false - if (activeSearch && activeSearch.response && activeSearch.response.plan) { - itineraries = getActiveItineraries(state.otp) - } else if (activeSearch && activeSearch.response && activeSearch.response.otp) { - profileOptions = activeSearch.response.otp.profile - itineraries = profileOptionsToItineraries(profileOptions) - showProfileSummary = true - } + const itineraries = activeSearch && activeSearch.response && activeSearch.response.plan + ? getActiveItineraries(state.otp) + : null - const pending = activeSearch && activeSearch.pending return { itineraries, - profileOptions, - pending, - showProfileSummary, + pending: activeSearch && activeSearch.pending, activeItinerary: activeSearch && activeSearch.activeItinerary, activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, companies: state.otp.currentQuery.companies, - timeFormat: getTimeFormat(state.otp.config) + timeFormat: coreUtils.time.getTimeFormat(state.otp.config) } } diff --git a/lib/components/narrative/leg-diagram-preview.js b/lib/components/narrative/leg-diagram-preview.js index 5a736e5b9..fd624d807 100644 --- a/lib/components/narrative/leg-diagram-preview.js +++ b/lib/components/narrative/leg-diagram-preview.js @@ -1,10 +1,10 @@ -import React, {Component} from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, {Component} from 'react' import { connect } from 'react-redux' import ReactResizeDetector from 'react-resize-detector' import { showLegDiagram } from '../../actions/map' -import { getElevationProfile } from '../../util/itinerary' const METERS_TO_FEET = 3.28084 @@ -45,7 +45,7 @@ class LegDiagramPreview extends Component { render () { const { leg, showElevationProfile } = this.props if (!showElevationProfile) return null - const profile = getElevationProfile(leg.steps) + const profile = coreUtils.itinerary.getElevationProfile(leg.steps) // Don't show for very short legs if (leg.distance < 500 || leg.mode === 'CAR') return null diff --git a/lib/components/narrative/line-itin/access-leg-body.js b/lib/components/narrative/line-itin/access-leg-body.js deleted file mode 100644 index 698651796..000000000 --- a/lib/components/narrative/line-itin/access-leg-body.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { VelocityTransitionGroup } from 'velocity-react' -import currencyFormatter from 'currency-formatter' - -import LegDiagramPreview from '../leg-diagram-preview' - -import { distanceString } from '../../../util/distance' -import { - getLegModeLabel, - getLegIcon, - getPlaceName, - getStepDirection, - getStepStreetName -} from '../../../util/itinerary' -import { formatDuration, formatTime } from '../../../util/time' -import { isMobile } from '../../../util/ui' - -import DirectionIcon from '../../icons/direction-icon' - -/** - * Component for access (e.g. walk/bike/etc.) leg in narrative itinerary. This - * particular component is used in the line-itin (i.e., trimet-mod-otp) version - * of the narrative itinerary. - */ -export default class AccessLegBody extends Component { - static propTypes = { - leg: PropTypes.object, - routingType: PropTypes.string - } - - constructor (props) { - super(props) - this.state = { expanded: false } - } - - _onStepsHeaderClick = () => { - this.setState({ expanded: !this.state.expanded }) - } - - _onSummaryClick = () => { - this.props.setActiveLeg(this.props.legIndex, this.props.leg) - } - - render () { - const { config, customIcons, followsTransit, leg, timeOptions } = this.props - - if (leg.mode === 'CAR' && leg.hailedCar) { - return ( - - ) - } - - return ( -
- - -
- {formatDuration(leg.duration)} - {leg.steps && } -
- - {this.props.routingType === 'ITINERARY' && } - - {this.state.expanded && } - -
- ) - } -} - -class TNCLeg extends Component { - render () { - // TODO: ensure that client ID fields are populated - const { - config, - LYFT_CLIENT_ID, - UBER_CLIENT_ID, - customIcons, - followsTransit, - leg, - timeOptions - } = this.props - const universalLinks = { - 'UBER': `https://m.uber.com/${isMobile() ? 'ul/' : ''}?client_id=${UBER_CLIENT_ID}&action=setPickup&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&pickup[formatted_address]=${encodeURI(leg.from.name)}&dropoff[latitude]=${leg.to.lat}&dropoff[longitude]=${leg.to.lon}&dropoff[formatted_address]=${encodeURI(leg.to.name)}`, - 'LYFT': `https://lyft.com/ride?id=lyft&partner=${LYFT_CLIENT_ID}&pickup[latitude]=${leg.from.lat}&pickup[longitude]=${leg.from.lon}&destination[latitude]=${leg.to.lat}&destination[longitude]=${leg.to.lon}` - } - const { tncData } = leg - - if (!tncData || !tncData.estimatedArrival) return null - return ( -
-
- Wait {!followsTransit && {Math.round(tncData.estimatedArrival / 60)} minutes }for {tncData.displayName} pickup -
- -
- {/* The icon/summary row */} - - - {/* The "Book Ride" button */} -
- - Book Ride - - {followsTransit &&
} - {followsTransit && ( -
-
-
- Wait until {formatTime(leg.startTime - tncData.estimatedArrival * 1000, timeOptions)} to book -
-
-
- )} -
- - {/* The estimated travel time */} -
- Estimated travel time: {formatDuration(leg.duration)} (does not account for traffic) -
- - {/* The estimated travel cost */} - {tncData.minCost && -

Estimated cost: { - `${currencyFormatter.format(tncData.minCost, { code: tncData.currency })} - ${currencyFormatter.format(tncData.maxCost, { code: tncData.currency })}` - }

- } -
-
- ) - } -} - -class AccessLegSummary extends Component { - render () { - const { config, customIcons, leg } = this.props - return ( -
- {/* Mode-specific icon */} -
{getLegIcon(leg, customIcons)}
- - {/* Leg description, e.g. "Walk 0.5 mi to..." */} -
- {getLegModeLabel(leg)} - {' '} - {leg.distance > 0 && {distanceString(leg.distance)}} - {` to ${getPlaceName(leg.to, config.companies)}`} -
-
- ) - } -} - -class AccessLegSteps extends Component { - static propTypes = { - steps: PropTypes.array - } - - render () { - return ( -
- {this.props.steps.map((step, k) => { - return
-
- -
- -
- {getStepDirection(step)} - {step.relativeDirection === 'ELEVATOR' ? ' to ' : ' on '} - - {getStepStreetName(step)} - -
-
- })} -
- ) - } -} diff --git a/lib/components/narrative/line-itin/connected-itinerary-body.js b/lib/components/narrative/line-itin/connected-itinerary-body.js new file mode 100644 index 000000000..5d36ac58f --- /dev/null +++ b/lib/components/narrative/line-itin/connected-itinerary-body.js @@ -0,0 +1,92 @@ +import isEqual from 'lodash.isequal' +import TransitLegSummary from '@opentripplanner/itinerary-body/lib/defaults/transit-leg-summary' +import ItineraryBody from '@opentripplanner/itinerary-body/lib/otp-react-redux/itinerary-body' +import LineColumnContent from '@opentripplanner/itinerary-body/lib/otp-react-redux/line-column-content' +import PlaceName from '@opentripplanner/itinerary-body/lib/otp-react-redux/place-name' +import { PlaceName as PlaceNameWrapper } from '@opentripplanner/itinerary-body/lib/styled' +import RouteDescription from '@opentripplanner/itinerary-body/lib/otp-react-redux/route-description' +import React, { Component } from 'react' +import { connect } from 'react-redux' +import styled from 'styled-components' + +import { showLegDiagram } from '../../../actions/map' +import { setActiveLeg } from '../../../actions/narrative' +import { setViewedTrip } from '../../../actions/ui' +import TransitLegSubheader from './connected-transit-leg-subheader' +import TripDetails from '../connected-trip-details' +import TripTools from '../trip-tools' + +const noop = () => {} + +const ItineraryBodyContainer = styled.div` + padding: 20px 0px; +` + +const StyledItineraryBody = styled(ItineraryBody)` + ${PlaceNameWrapper} { + font-weight: inherit; + } +` + +class ConnectedItineraryBody extends Component { + /** avoid rerendering if the itinerary to display hasn't changed */ + shouldComponentUpdate (nextProps, nextState) { + return !isEqual(this.props.itinerary, nextProps.itinerary) + } + + render () { + const { + config, + diagramVisible, + itinerary, + LegIcon, + setActiveLeg, + setViewedTrip, + showLegDiagram + } = this.props + + return ( + + + + + + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + config: state.otp.config, + diagramVisible: state.otp.ui.diagramLeg + } +} + +const mapDispatchToProps = { + setActiveLeg, + setViewedTrip, + showLegDiagram +} + +export default connect(mapStateToProps, mapDispatchToProps)( + ConnectedItineraryBody +) diff --git a/lib/components/narrative/line-itin/connected-transit-leg-subheader.js b/lib/components/narrative/line-itin/connected-transit-leg-subheader.js new file mode 100644 index 000000000..130374022 --- /dev/null +++ b/lib/components/narrative/line-itin/connected-transit-leg-subheader.js @@ -0,0 +1,33 @@ +import TransitLegSubheader from '@opentripplanner/itinerary-body/lib/otp-react-redux/transit-leg-subheader' +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { setMainPanelContent, setViewedStop } from '../../../actions/ui' + +class ConnectedTransitLegSubheader extends Component { + onClick = (payload) => { + const { setMainPanelContent, setViewedStop } = this.props + setMainPanelContent(null) + setViewedStop(payload) + } + + render () { + const { languageConfig, leg } = this.props + return ( + + ) + } +} + +const mapDispatchToProps = { + setMainPanelContent, + setViewedStop +} + +export default connect(null, mapDispatchToProps)( + ConnectedTransitLegSubheader +) diff --git a/lib/components/narrative/line-itin/itin-body.js b/lib/components/narrative/line-itin/itin-body.js deleted file mode 100644 index c38ced30d..000000000 --- a/lib/components/narrative/line-itin/itin-body.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import isEqual from 'lodash.isequal' - -import TripDetails from '../trip-details' -import TripTools from '../trip-tools' - -import PlaceRow from './place-row' - -export default class ItineraryBody extends Component { - static propTypes = { - companies: PropTypes.string, - itinerary: PropTypes.object, - routingType: PropTypes.string - } - - constructor (props) { - super(props) - this.rowKey = 0 - } - - shouldComponentUpdate (nextProps, nextState) { - return !isEqual(this.props.companies, nextProps.companies) || - !isEqual(this.props.itinerary, nextProps.itinerary) - } - - render () { - const { itinerary, setActiveLeg, timeOptions } = this.props - - const rows = [] - let followsTransit = false - itinerary.legs.forEach((leg, i) => { - // Create a row containing this leg's start place and leg traversal details - rows.push( - - ) - // If this is the last leg, create a special PlaceRow for the destination only - if (i === itinerary.legs.length - 1) { - rows.push( - ) - } - if (leg.transitLeg) followsTransit = true - }) - - return ( -
- {rows} - - -
- ) - } -} diff --git a/lib/components/narrative/line-itin/itin-summary.js b/lib/components/narrative/line-itin/itin-summary.js index e8965fe35..b402e2cba 100644 --- a/lib/components/narrative/line-itin/itin-summary.js +++ b/lib/components/narrative/line-itin/itin-summary.js @@ -1,15 +1,78 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' - -import { calculateFares, calculatePhysicalActivity, getLegIcon, isTransit } from '../../../util/itinerary' -import { formatDuration, formatTime } from '../../../util/time' +import React, { Component } from 'react' +import styled from 'styled-components' // TODO: make this a prop const defaultRouteColor = '#008' +const Container = styled.div` + display: ${() => coreUtils.ui.isMobile() ? 'table' : 'none'}; + height: 60px; + margin-bottom: 15px; + padding-right: 5px; + width: 100%; +` + +const Detail = styled.div` + color: #999999; + font-size: 13px; +` + +const Details = styled.div` + display: table-cell; + vertical-align: top; +` + +const Header = styled.div` + font-size: 18px; + font-weight: bold; + margin-top: -3px; +` + +const LegIconContainer = styled.div` + height: 30px; + width: 30px; +` + +const NonTransitSpacer = styled.div` + height: 30px; + overflow: hidden +` + +const RoutePreview = styled.div` + display: inline-block; + margin-left: 8px; + vertical-align: top; +` + +const Routes = styled.div` + display: table-cell; + text-align: right; +` + +const ShortName = styled.div` + background-color: ${props => getRouteColorForBadge(props.leg)}; + border-radius: 15px; + border: 2px solid white; + box-shadow: 0 0 0.5em #000; + color: white; + font-size: 15px; + font-weight: 500; + height: 30px; + margin-top: 6px; + overflow: hidden; + padding-top: 4px; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + width: 30px; +` + export default class ItinerarySummary extends Component { static propTypes = { - itinerary: PropTypes.object + itinerary: PropTypes.object, + LegIcon: PropTypes.elementType.isRequired } _onSummaryClicked = () => { @@ -17,66 +80,68 @@ export default class ItinerarySummary extends Component { } render () { - const { customIcons, itinerary, timeOptions } = this.props + const { itinerary, LegIcon, timeOptions } = this.props const { centsToString, maxTNCFare, minTNCFare, transitFare - } = calculateFares(itinerary) + } = coreUtils.itinerary.calculateFares(itinerary) // TODO: support non-USD const minTotalFare = minTNCFare * 100 + transitFare const maxTotalFare = maxTNCFare * 100 + transitFare - const { caloriesBurned } = calculatePhysicalActivity(itinerary) + const { caloriesBurned } = coreUtils.itinerary.calculatePhysicalActivity(itinerary) return ( -
-
+ +
{/* Travel time in hrs/mins */} -
{formatDuration(itinerary.duration)}
+
{coreUtils.time.formatDuration(itinerary.duration)}
{/* Duration as time range */} -
- {formatTime(itinerary.startTime, timeOptions)} - {formatTime(itinerary.endTime, timeOptions)} -
+ + {coreUtils.time.formatTime(itinerary.startTime, timeOptions)} - {coreUtils.time.formatTime(itinerary.endTime, timeOptions)} + {/* Fare / Calories */} -
+ {minTotalFare > 0 && {centsToString(minTotalFare)} {minTotalFare !== maxTotalFare && - {centsToString(maxTotalFare)}} } {Math.round(caloriesBurned)} Cals -
+ {/* Number of transfers, if applicable */} {itinerary.transfers > 0 && ( -
+ {itinerary.transfers} transfer{itinerary.transfers > 1 ? 's' : ''} -
+ )} -
-
+ + {itinerary.legs.filter(leg => { return !(leg.mode === 'WALK' && itinerary.transitTime > 0) }).map((leg, k) => { - return
-
{getLegIcon(leg, customIcons)}
- {isTransit(leg.mode) - ? ( -
- {getRouteNameForBadge(leg)} -
- ) - : (
) - } -
+ return ( + + + {coreUtils.itinerary.isTransit(leg.mode) + ? ( + + {getRouteNameForBadge(leg)} + + ) + : () + } + + ) })} -
-
+ + ) } } diff --git a/lib/components/narrative/line-itin/itinerary.css b/lib/components/narrative/line-itin/itinerary.css deleted file mode 100644 index 89df3acf0..000000000 --- a/lib/components/narrative/line-itin/itinerary.css +++ /dev/null @@ -1,375 +0,0 @@ -.otp .options.profile .itin-body .place-row { - margin-left: 55px; -} - -.otp .line-itin { - margin-bottom: 20px; -} - -/* Itinerary summary */ - -.otp .line-itin .itin-summary { - padding-right: 5px; - height: 60px; - display: table; - width: 100%; - margin-bottom: 15px; -} - -.otp .desktop-narrative-container .options.itinerary .line-itin .itin-summary { - display: none; -} - -.otp .line-itin .itin-summary .details { - display: table-cell; - vertical-align: top; -} - -.otp .line-itin .itin-summary .header { - font-weight: bold; - font-size: 18px; - margin-top: -3px; -} - -.otp .line-itin .itin-summary .detail { - font-size: 13px; - color: #999999; -} - -.otp .line-itin .itin-summary .routes { - display: table-cell; - text-align: right; -} - -.otp .line-itin .itin-summary .routes .route-preview { - display: inline-block; - margin-left: 8px; - vertical-align: top; -} - -.otp .line-itin .itin-summary .routes .route-preview .mode-icon { - height: 30px; - width: 30px; -} - -.otp .line-itin .itin-summary .routes .route-preview .short-name { - color: white; - font-weight: 500; - text-align: center; - margin-top: 6px; - font-size: 15px; - padding-top: 2px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - width: 30px; - height: 30px; - border-radius: 15px; - border: 2px solid white; - box-shadow: 0 0 0.5em #000; -} - -/* Itinerary main body */ - -.otp .line-itin .itin-body { - padding: 20px 0px; -} - -.otp .line-itin .place-row { - display: table; - width: 100%; -} - - -/* Departure/arrival time (1st column in table) */ - -.otp .line-itin .time { - display: table-cell; - width: 60px; - font-size: 14px; - color: #999999; - text-align: right; - padding-right: 4px; - padding-top: 1px; - vertical-align: top; -} - -/* The place icon and line itself (2nd column in table) */ -.otp .line-itin .line-container { - position: relative; - display: table-cell; - width: 20px; - max-width: 20px; -} - -.otp .line-itin .place-icon-group { - position: absolute; - font-size: 18px; - left: -8px; - top: -7px; - z-index: 20; -} - -.otp .line-itin .leg-line { - position: absolute; - top: 11px; - bottom: -11px; - z-index: 10; -} - -// Internet explorer specific media query to apply the below styling to fix -// rendering issues with table cell display with undefined heights. -/*@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { - .otp .line-itin .line-container { - overflow: hidden; // hack for IE to render table cell correctly. - } - - .otp .line-itin .leg-line { - height: 1000px; // hack for IE to render table cell correctly. - } -}*/ - -.otp .line-itin .leg-line-walk { - left: 6px; - right: 6px; - background: radial-gradient(ellipse at center, #87cefa 40%, transparent 10%); - background-size: 12px 12px; - background-repeat: repeat-y; - background-position: center -5px; -} - -.otp .line-itin .leg-line-bicycle { - left: 7.5px; - right: 7.5px; - background: repeating-linear-gradient( - 0deg, - red, - red 8px, - white 8px, - white 12.5px - ); -} - -.otp .line-itin .leg-line-car { - left: 7.5px; - right: 7.5px; - background: repeating-linear-gradient( - 0deg, - grey, - grey 8px, - white 8px, - white 12.5px - ); -} - -.otp .line-itin .leg-line-micromobility { - left: 7.5px; - right: 7.5px; - background: repeating-linear-gradient( - 0deg, - #f5a729, - #f5a729 8px, - white 8px, - white 12.5px - ); -} - -.otp .line-itin .leg-line-transit { - left: 5px; - right: 5px; - background-color: gray; -} - -/* Place/Leg details (3rd column in table) */ - -.otp .line-itin .place-details { - font-size: 13px; - display: table-cell; - padding-top: 1px; -} - -.otp .line-itin .place-name { - font-size: 18px; - line-height: 20px; - padding-left: 4px; - font-weight: 500; - color: black; -} - -.otp .line-itin .place-subheader { - font-size: 12px; - padding-left: 4px; - padding-top: 1px; - font-weight: 300; - color: gray; -} - -.otp .line-itin .interline-dot { - position: relative; - float: left; - margin-left: -13.5px; - z-index: 25; - color: #fff; -} - -.otp .line-itin .interline-name { - font-size: 14px; - font-weight: 400; - line-height: 16px; -} - -/* Leg body general */ - -.otp .line-itin .leg-body { - padding: 12px 0px 18px 4px; - font-size: 13px; - color: #999999; -} - -.otp .line-itin .summary { - cursor: pointer; -} - -.otp .line-itin .leg-body .icon { - height: 24px; - width: 24px; - float: left; - margin-right: 6px; -} - -.otp .line-itin .leg-body .leg-description { - display: table; -} - -.otp .line-itin .leg-body .leg-description > div { - display: table-cell; - vertical-align: middle; -} - -/* Leg steps (for turn-by-turn) */ - -.otp .line-itin .leg-body .steps-header { - font-size: 13px; - margin-top: 10px; - color: #999999; - font-style: normal; - display: inline-block; -} - -.otp .line-itin .leg-body .step-row { - font-size: 13px; - margin-top: 8px; - color: #999999; - font-style: normal; -} - -/* Transit leg details */ - -.otp .line-itin .leg-body .route-name { - color: #999999; - margin-top: 5px; -} - -.otp .line-itin .leg-body .route-short-name { - display: inline-block; - background-color: #0f6aac; - padding-top: 1px; - color: white; - font-weight: 500; - font-size: 14px; - margin-right: 6px; - text-align: center; - width: 24px; - height: 24px; - border-radius: 12px; - border: 1px solid white; - box-shadow: 0 0 0.25em #000; - margin-right: 8px; -} - -.otp .line-itin .leg-body .route-long-name { - font-size: 13px; - line-height: 16px; - font-weight: 500; -} - -.otp .line-itin .leg-body .transit-leg-details { - margin-top: 5px; -} - -.otp .line-itin .leg-body .agency-info { - margin-top: 5px; - -} - -.otp .line-itin .leg-body .transit-leg-details .header { - cursor: pointer; - color: #999999; - font-size: 13px; -} - -/* Intermediate stops */ - -.otp .line-itin .leg-body .transit-leg-details .intermediate-stops .stop-row { - z-index: 30; - position: relative; -} - -.otp .line-itin .leg-body .transit-leg-details .intermediate-stops .stop-marker { - float: left; - margin-left: -17px; - color: white; -} - -.otp .line-itin .leg-body .transit-leg-details .intermediate-stops .stop-name { - color: #999999; - font-size: 14px; - margin-top: 3px; -} - -/* Transit alerts */ - -.otp .line-itin .leg-body .transit-alerts-toggle { - display: inline-block; - margin-top: 8px; - color: #D14727; - font-weight: 400; - cursor: pointer; -} - -.otp .line-itin .leg-body .transit-alerts { - margin-top: 3px; -} - -.otp .line-itin .leg-body .transit-alerts .transit-alert { - margin-top: 5px; - background-color: #eee; - padding: 8px; - color: black; - border-radius: 4px; -} - -.otp .line-itin .leg-body .transit-alerts .transit-alert .alert-icon { - float: left; - font-size: 18px; -} - -.otp .line-itin .leg-body .transit-alerts .transit-alert .alert-header { - font-size: 14px; - margin-left: 30px; - font-weight: 600; -} - -.otp .line-itin .leg-body .transit-alerts .transit-alert .alert-body { - font-size: 12px; - margin-left: 30px; - /* white space pre-wrap is required to render line breaks correctly. */ - white-space: pre-wrap; -} - -.otp .line-itin .leg-body .transit-alerts .transit-alert .effective-date { - margin-top: 5px; - margin-left: 30px; - font-size: 12px; - font-style: italic; -} diff --git a/lib/components/narrative/line-itin/line-itinerary.js b/lib/components/narrative/line-itin/line-itinerary.js index 6c89ddae0..d0550d1f0 100644 --- a/lib/components/narrative/line-itin/line-itinerary.js +++ b/lib/components/narrative/line-itin/line-itinerary.js @@ -1,11 +1,17 @@ +import coreUtils from '@opentripplanner/core-utils' import React from 'react' +import styled from 'styled-components' +import ItineraryBody from './connected-itinerary-body' +import ItinerarySummary from './itin-summary' import NarrativeItinerary from '../narrative-itinerary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' -import { getLegModeLabel, getTimeZoneOffset, isTransit } from '../../../util/itinerary' -import ItinerarySummary from './itin-summary' -import ItineraryBody from './itin-body' +const { getLegModeLabel, getTimeZoneOffset, isTransit } = coreUtils.itinerary + +export const LineItineraryContainer = styled.div` + margin-bottom: 20px; +` export default class LineItinerary extends NarrativeItinerary { _headerText () { @@ -40,10 +46,10 @@ export default class LineItinerary extends NarrativeItinerary { const { active, companies, - customIcons, expanded, itinerary, itineraryFooter, + LegIcon, showRealtimeAnnotation, onClick, timeFormat @@ -59,22 +65,20 @@ export default class LineItinerary extends NarrativeItinerary { } return ( -
+ + /> {showRealtimeAnnotation && } {active || expanded - ? + ? : null} {itineraryFooter} -
+ ) } } diff --git a/lib/components/narrative/line-itin/place-row.js b/lib/components/narrative/line-itin/place-row.js deleted file mode 100644 index cdb1526d2..000000000 --- a/lib/components/narrative/line-itin/place-row.js +++ /dev/null @@ -1,216 +0,0 @@ -import React, { Component, PureComponent } from 'react' -import { connect } from 'react-redux' - -import LocationIcon from '../../icons/location-icon' -import ViewStopButton from '../../viewers/view-stop-button' -import { - getCompaniesLabelFromNetworks, - getModeForPlace, - getPlaceName -} from '../../../util/itinerary' -import { formatTime } from '../../../util/time' - -import TransitLegBody from './transit-leg-body' -import AccessLegBody from './access-leg-body' - -// TODO: make this a prop -const defaultRouteColor = '#008' - -class PlaceRow extends Component { - _createLegLine (leg) { - switch (leg.mode) { - case 'WALK': return
- case 'BICYCLE': - case 'BICYCLE_RENT': - return
- case 'CAR': return
- case 'MICROMOBILITY': - case 'MICROMOBILITY_RENT': - return
- default: - return
- } - } - - /* eslint-disable complexity */ - render () { - const { config, customIcons, leg, legIndex, place, time, timeOptions, followsTransit } = this.props - const stackIcon = (name, color, size) => - let icon - if (!leg) { // This is the itinerary destination - icon = ( - - {stackIcon('circle', 'white', 26)} - - - ) - } else if (legIndex === 0) { // The is the origin - icon = ( - - {stackIcon('circle', 'white', 26)} - - - ) - } else { // This is an intermediate place - icon = ( - - {stackIcon('circle', 'white', 22)} - {stackIcon('circle-o', 'black', 22)} - - ) - } - // NOTE: Previously there was a check for itineraries that changed vehicles - // at a single stop, which would render the stop place the same as the - // interline stop. However, this prevents the user from being able to click - // on the stop viewer in this case, which they may want to do in order to - // check the real-time arrival information for the next leg of their journey. - const interline = leg && leg.interlineWithPreviousLeg - return ( -
-
- {time && formatTime(time, timeOptions)} -
-
- {leg && this._createLegLine(leg) } -
{!interline && icon}
-
-
- {/* Dot separating interlined segments, if applicable */} - {interline &&
} - - {/* The place name */} -
- {interline - ?
Stay on Board at {place.name}
- :
{getPlaceName(place, config.companies)}
- } -
- - {/* Place subheading: Transit stop */} - {place.stopId && !interline && ( -
- Stop ID {place.stopId.split(':')[1]} - -
- )} - - {/* Place subheading: rented vehicle (e.g., scooter, bike, car) pickup */} - {leg && (leg.rentedVehicle || leg.rentedBike || leg.rentedCar) && ( - - )} - - {/* Show the leg, if present */} - {leg && ( - leg.transitLeg - ? (/* This is a transit leg */ - - ) - : (/* This is an access (e.g. walk/bike/etc.) leg */ - - ) - )} -
-
- ) - } -} - -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - // Pass config in order to give access to companies definition (used to - // determine proper place names for rental vehicles). - config: state.otp.config - } -} - -const mapDispatchToProps = { } - -export default connect(mapStateToProps, mapDispatchToProps)(PlaceRow) - -/** - * A component to display vehicle rental data. The word "Vehicle" has been used - * because a future refactor is intended to combine car rental, bike rental - * and micromobility rental all within this component. The future refactor is - * assuming that the leg.rentedCar and leg.rentedBike response elements from OTP - * will eventually be merged into the leg.rentedVehicle element. - */ -class RentedVehicleLeg extends PureComponent { - render () { - const { config, leg } = this.props - const configCompanies = config.companies || [] - - // Sometimes rented vehicles can be walked over things like stairs or other - // ways that forbid the main mode of travel. - if (leg.mode === 'WALK') { - return ( -
- Walk vehicle along {leg.from.name} -
- ) - } - - let rentalDescription = 'Pick up' - if (leg.rentedBike) { - // TODO: Special case for TriMet may need to be refactored. - rentalDescription += ` shared bike` - } else { - // Add company and vehicle labels. - let vehicleName = '' - // TODO allow more flexibility in customizing these mode strings - let modeString = leg.rentedVehicle - ? 'E-scooter' - : leg.rentedBike - ? 'bike' - : 'car' - - // The networks attribute of the from data will only appear at the very - // beginning of the rental. It is possible that there will be some forced - // walking that occurs in the middle of the rental, so once the main mode - // resumes there won't be any network info. In that case we simply return - // that the rental is continuing. - if (leg.from.networks) { - const companiesLabel = getCompaniesLabelFromNetworks( - leg.from.networks, - configCompanies - ) - rentalDescription += ` ${companiesLabel}` - // Only show vehicle name for car rentals. For bikes and E-scooters, these - // IDs/names tend to be less relevant (or entirely useless) in this context. - if (leg.rentedCar && leg.from.name) { - vehicleName = leg.from.name - } - modeString = getModeForPlace(leg.from) - } else { - rentalDescription = 'Continue using rental' - } - - rentalDescription += ` ${modeString} ${vehicleName}` - } - // e.g., Pick up REACHNOW rented car XYZNDB OR - // Pick up SPIN E-scooter - // Pick up shared bike - return ( -
- {rentalDescription} -
- ) - } -} diff --git a/lib/components/narrative/line-itin/transit-leg-body.js b/lib/components/narrative/line-itin/transit-leg-body.js deleted file mode 100644 index a1fafdb1d..000000000 --- a/lib/components/narrative/line-itin/transit-leg-body.js +++ /dev/null @@ -1,237 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import PropTypes from 'prop-types' -import { VelocityTransitionGroup } from 'velocity-react' -import moment from 'moment' - -import ViewTripButton from '../../viewers/view-trip-button' -import { getIcon } from '../../../util/itinerary' -import { - formatDuration, - getLongDateFormat, - getTimeFormat -} from '../../../util/time' - -// TODO: support multi-route legs for profile routing - -class TransitLegBody extends Component { - static propTypes = { - leg: PropTypes.object, - legIndex: PropTypes.number, - setActiveLeg: PropTypes.func - } - - constructor (props) { - super(props) - this.state = { - alertsExpanded: false, - stopsExpanded: false - } - } - - _onToggleStopsClick = () => { - this.setState({ stopsExpanded: !this.state.stopsExpanded }) - } - - _onToggleAlertsClick = () => { - this.setState({ alertsExpanded: !this.state.alertsExpanded }) - } - - _onSummaryClick = () => { - this.props.setActiveLeg(this.props.legIndex, this.props.leg) - } - - render () { - const { customIcons, leg, longDateFormat, operator, timeFormat } = this.props - const { - agencyBrandingUrl, - agencyName, - agencyUrl, - alerts, - mode, - routeShortName, - routeLongName, - headsign - } = leg - const { alertsExpanded, stopsExpanded } = this.state - - // If the config contains an operator with a logo URL, prefer that over the - // one provided by OTP (which is derived from agency.txt#agency_branding_url) - const logoUrl = operator && operator.logo ? operator.logo : agencyBrandingUrl - - // get the iconKey for the leg's icon - let iconKey = mode - if (typeof customIcons.customIconForLeg === 'function') { - const customIcon = customIcons.customIconForLeg(leg) - if (customIcon) iconKey = customIcon - } - - return ( -
- {/* The Route Icon/Name Bar; clickable to set as active leg */} -
-
-
-
{getIcon(iconKey, customIcons)}
-
- {routeShortName && ( -
- {routeShortName} -
- )} -
- {routeLongName} - {headsign && to {headsign}} -
-
-
- - {/* Agency information */} - { -
- Service operated by{' '} - - {agencyName}{logoUrl && - - } - -
- } - - {/* Alerts toggle */} - {alerts && alerts.length > 0 && ( -
- {alerts.length} {pluralize('alert', alerts)} - {' '} - -
- )} - - {/* The Alerts body, if visible */} - - {alertsExpanded && - - } - - {/* The "Ride X Min / X Stops" Row, including IntermediateStops body */} - {leg.intermediateStops && leg.intermediateStops.length > 0 && ( -
- - {/* The header summary row, clickable to expand intermediate stops */} -
- {leg.duration && Ride {formatDuration(leg.duration)}} - {leg.intermediateStops && ( - - {' / '} - {leg.intermediateStops.length + 1} - {' stops '} - - - )} - - {/* The ViewTripButton. TODO: make configurable */} - -
- {/* IntermediateStops expanded body */} - - {stopsExpanded ? : null } - - - {/* Average wait details, if present */} - {leg.averageWait && Typical Wait: {formatDuration(leg.averageWait)}} -
- )} -
- ) - } -} - -// Connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return { - longDateFormat: getLongDateFormat(state.otp.config), - operator: state.otp.config.operators.find(operator => operator.id === ownProps.leg.agencyId), - timeFormat: getTimeFormat(state.otp.config) - } -} - -const mapDispatchToProps = {} - -export default connect(mapStateToProps, mapDispatchToProps)(TransitLegBody) - -class IntermediateStops extends Component { - static propTypes = { - stops: PropTypes.array - } - - render () { - return ( -
- {this.props.stops.map((stop, k) => { - return
-
-
{stop.name}
-
- })} -
- ) - } -} - -class AlertsBody extends Component { - static propTypes = { - alerts: PropTypes.array - } - - render () { - const { longDateFormat, timeFormat } = this.props - return ( -
- {this.props.alerts - .sort((a, b) => b.effectiveStartDate - a.effectiveStartDate) - .map((alert, i) => { - // If alert is effective as of +/- one day, use today, tomorrow, or - // yesterday with time. Otherwise, use long date format. - const dateTimeString = moment(alert.effectiveStartDate) - .calendar(null, { - sameDay: `${timeFormat}, [Today]`, - nextDay: `${timeFormat}, [Tomorrow]`, - lastDay: `${timeFormat}, [Yesterday]`, - lastWeek: `${longDateFormat}`, - sameElse: `${longDateFormat}` - }) - const effectiveDateString = `Effective as of ${dateTimeString}` - return ( -
-
- {alert.alertHeaderText - ?
{alert.alertHeaderText}
- : null - } -
{alert.alertDescriptionText}
-
{effectiveDateString}
-
- ) - }) - } -
- ) - } -} - -// TODO use pluralize that for internationalization (and complex plurals, i.e., not just adding 's') -function pluralize (str, list) { - return `${str}${list.length > 1 ? 's' : ''}` -} diff --git a/lib/components/narrative/narrative-profile-options.js b/lib/components/narrative/narrative-profile-options.js deleted file mode 100644 index 405d6609a..000000000 --- a/lib/components/narrative/narrative-profile-options.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' - -import { setActiveItinerary, setActiveLeg, setActiveStep } from '../../actions/narrative' -import DefaultItinerary from './default/default-itinerary' -import NarrativeProfileSummary from './narrative-profile-summary' -import Loading from './loading' -import { getActiveSearch } from '../../util/state' -import { profileOptionsToItineraries } from '../../util/profile' - -class NarrativeProfileOptions extends Component { - static propTypes = { - options: PropTypes.array, - query: PropTypes.object, - itineraryClass: PropTypes.func, - pending: PropTypes.bool, - activeOption: PropTypes.number, - setActiveItinerary: PropTypes.func, - setActiveLeg: PropTypes.func, - setActiveStep: PropTypes.func, - customIcons: PropTypes.object - } - - static defaultProps = { - itineraryClass: DefaultItinerary - } - - render () { - const { pending, itineraryClass, query, activeItinerary } = this.props - if (pending) return - - const options = this.props.options - if (!options) return null - - const itineraries = profileOptionsToItineraries(options, query) - - return ( -
-
Your best options:
- -
We found {options.length} total options:
- {itineraries.map((itinerary, index) => { - return React.createElement(itineraryClass, { - itinerary, - index, - key: index, - active: index === activeItinerary, - routingType: 'PROFILE', - ...this.props - }) - })} -
- ) - } -} - -// connect to the redux store -const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - // const { activeItinerary, activeLeg, activeStep } = activeSearch ? activeSearch.activeItinerary : {} - const pending = activeSearch && activeSearch.pending - return { - options: - activeSearch && - activeSearch.response && - activeSearch.response.otp - ? activeSearch.response.otp.profile - : null, - pending, - activeItinerary: activeSearch && activeSearch.activeItinerary, - activeLeg: activeSearch && activeSearch.activeLeg, - activeStep: activeSearch && activeSearch.activeStep, - query: activeSearch && activeSearch.query - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - setActiveItinerary: (index) => { dispatch(setActiveItinerary({ index })) }, - setActiveLeg: (index, leg) => { dispatch(setActiveLeg({ index, leg })) }, - setActiveStep: (index, step) => { dispatch(setActiveStep({ index, step })) } - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(NarrativeProfileOptions) diff --git a/lib/components/narrative/narrative-profile-summary.js b/lib/components/narrative/narrative-profile-summary.js deleted file mode 100644 index cf8db6c6e..000000000 --- a/lib/components/narrative/narrative-profile-summary.js +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -import { getIcon } from '../../util/itinerary' - -export default class NarrativeProfileSummary extends Component { - static propTypes = { - options: PropTypes.array, - customIcons: PropTypes.object - } - - render () { - const { options } = this.props - - let bestTransit = 0 - let walk = 0 - let bicycle = 0 - let bicycleRent = 0 - - options.forEach((option, i) => { - if (option.transit) { - if (option.time < bestTransit || bestTransit === 0) { - bestTransit = option.time - } - } else { - if (option.modes.length === 1 && option.modes[0] === 'bicycle') bicycle = option.time - else if (option.modes.length === 1 && option.modes[0] === 'walk') walk = option.time - else if (option.modes.indexOf('bicycle_rent') !== -1) bicycleRent = option.time - } - }) - - const summary = [ - { - icon: 'BUS', - title: 'Transit', - time: bestTransit - }, { - icon: 'BICYCLE', - title: 'Bicycle', - time: bicycle - }, { - icon: 'BICYCLE_RENT', - title: 'Bikeshare', - time: bicycleRent - }, { - icon: 'WALK', - title: 'Walk', - time: walk - } - ] - - return ( -
- {summary.map((option, k) => { - return ( -
0 ? '#084C8D' : '#bbb', - width: '22%', - display: 'inline-block', - verticalAlign: 'top', - marginRight: (k < 3 ? '4%' : 0), - padding: '3px', - textAlign: 'center', - color: 'white' }} - > -
{getIcon(option.icon, this.props.customIcons)}
-
{option.title}
-
- {option.time > 0 - ? {Math.round(option.time / 60)} min - : (Not Found) - } -
-
- ) - })} -
- ) - } -} diff --git a/lib/components/narrative/narrative-routing-results.js b/lib/components/narrative/narrative-routing-results.js index f7f407420..f16634d26 100644 --- a/lib/components/narrative/narrative-routing-results.js +++ b/lib/components/narrative/narrative-routing-results.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import Loading from './loading' -import NarrativeProfileOptions from './narrative-profile-options' import TabbedItineraries from './tabbed-itineraries' import ErrorMessage from '../form/error-message' @@ -12,8 +11,8 @@ import { setMainPanelContent } from '../../actions/ui' class NarrativeRoutingResults extends Component { static propTypes = { - customIcons: PropTypes.object, itineraryClass: PropTypes.func, + LegIcon: PropTypes.elementType.isRequired, routingType: PropTypes.string } @@ -26,18 +25,19 @@ class NarrativeRoutingResults extends Component { } render () { - const { customIcons, error, itineraryClass, itineraryFooter, pending, routingType, itineraries, mainPanelContent } = this.props + const { error, itineraryClass, itineraryFooter, LegIcon, pending, itineraries, mainPanelContent } = this.props if (pending) return if (error) return if (mainPanelContent) return null return ( - routingType === 'ITINERARY' - ? - : + // TODO: If multiple routing types exist, do the check here. + ) } } diff --git a/lib/components/narrative/narrative.css b/lib/components/narrative/narrative.css index 21c1ab760..ac8c4effa 100644 --- a/lib/components/narrative/narrative.css +++ b/lib/components/narrative/narrative.css @@ -116,58 +116,6 @@ fontSize: 16px; } -/* TRIP DETAILS */ - -.otp .trip-details { - border: 2px solid gray; - padding: 6px 10px; - margin-top: 16px; -} - -.otp .trip-details .trip-details-header { - font-size: 18px; - font-weight: 600; -} - -.otp .trip-details .trip-detail { - margin-top: 6px; -} - -.otp .trip-details .trip-detail .icon { - float: left; - font-size: 17px; -} - -.otp .trip-details .trip-detail .summary { - margin-left: 28px; - padding-top: 2px; -} - -.otp .trip-details .trip-detail .expand-button { - margin-left: 6px; - margin-top: -2px; - font-size: 16px; - color: blue; -} - -.otp .trip-details .trip-detail .description { - background-color: #fff; - border: 1px solid #888; - padding: 8px; - margin-top: 2px; - font-size: 12px; -} - -.otp .trip-details .trip-detail b { - font-weight: 600; -} - -.otp .trip-details .trip-detail .hide-button { - float: right; - top: 5; - right: 5; -} - /* TRIP TOOLS ROW */ .otp .trip-tools { margin-top: 10px; diff --git a/lib/components/narrative/printable/itinerary.css b/lib/components/narrative/printable/itinerary.css deleted file mode 100644 index 07b6c9dfe..000000000 --- a/lib/components/narrative/printable/itinerary.css +++ /dev/null @@ -1,33 +0,0 @@ -.otp .printable-itinerary .leg { - margin-bottom: 10px; - border-top: 1px solid gray; - padding-top: 18px; -} - -.otp .printable-itinerary .leg.collapse-top { - border-top: none; - padding-top: 0px; -} - -.otp .printable-itinerary .mode-icon { - float: left; - width: 32px; - height: 32px; -} - -.otp .printable-itinerary .leg-body { - margin-left: 40px; -} - -.otp .printable-itinerary .leg-header { - font-size: 18px; -} - -.otp .printable-itinerary .leg-details { - margin-top: 5px; -} - -.otp .printable-itinerary .leg-detail { - margin-top: 3px; - font-size: 14px; -} diff --git a/lib/components/narrative/printable/printable-itinerary.js b/lib/components/narrative/printable/printable-itinerary.js deleted file mode 100644 index ecb13c545..000000000 --- a/lib/components/narrative/printable/printable-itinerary.js +++ /dev/null @@ -1,221 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -import TripDetails from '../trip-details' -import { distanceString } from '../../../util/distance' -import { formatTime, formatDuration } from '../../../util/time' -import { - getCompaniesLabelFromNetworks, - getLegIcon, - getLegModeLabel, - getPlaceName, - getStepDirection, - getStepStreetName, - getTimeZoneOffset -} from '../../../util/itinerary' - -export default class PrintableItinerary extends Component { - static propTypes = { - itinerary: PropTypes.object - } - - render () { - const { - configCompanies, - customIcons, - itinerary, - timeFormat - } = this.props - - const timeOptions = { - format: timeFormat, - offset: getTimeZoneOffset(itinerary) - } - - return ( -
- {itinerary.legs.length > 0 && ( -
-
-
- Depart from {itinerary.legs[0].from.name} -
-
-
- )} - {itinerary.legs.map((leg, k) => leg.transitLeg - ? - : leg.hailedCar - ? - : - )} - -
- ) - } -} - -class TransitLeg extends Component { - static propTypes = { - leg: PropTypes.object - } - - render () { - const { customIcons, leg, interlineFollows, timeOptions } = this.props - - // Handle case of transit leg interlined w/ previous - if (leg.interlineWithPreviousLeg) { - return ( -
-
-
- Continues as{' '} - {leg.routeShortName} {leg.routeLongName}{' '} - to {leg.to.name} -
-
-
- Get off at {leg.to.name}{' '} - at {formatTime(leg.endTime, timeOptions)} -
-
-
-
- ) - } - - return ( -
-
{getLegIcon(leg, customIcons)}
-
-
- {leg.routeShortName} {leg.routeLongName} to {leg.to.name} -
-
-
- Board at {leg.from.name}{' '} - at {formatTime(leg.startTime, timeOptions)} -
-
- {interlineFollows - ? Stay on board at {leg.to.name} - : - Get off at {leg.to.name}{' '} - at {formatTime(leg.endTime, timeOptions)} - - } -
-
-
-
- ) - } -} - -class AccessLeg extends Component { - static propTypes = { - leg: PropTypes.object - } - - render () { - const { configCompanies, customIcons, leg } = this.props - - // calculate leg mode label in a special way for this component - let legModeLabel = getLegModeLabel(leg) - - if (leg.rentedBike) { - // FIXME: Special case for TriMet that needs to be refactored to - // incorporate actual company. - legModeLabel = 'Ride BIKETOWN bike' - } else if (leg.rentedCar) { - // Add extra information to printview that would otherwise clutter up - // other places that use the getLegModeLabel function - const companiesLabel = getCompaniesLabelFromNetworks( - leg.from.networks, - configCompanies - ) - legModeLabel = `Drive ${companiesLabel} ${leg.from.name}` - } else if (leg.rentedVehicle) { - const companiesLabel = getCompaniesLabelFromNetworks( - leg.from.networks, - configCompanies - ) - legModeLabel = `Ride ${companiesLabel} E-scooter` - } - - return ( -
-
{getLegIcon(leg, customIcons)}
-
-
- {legModeLabel}{' '} - {!leg.hailedCar && - leg.distance > 0 && - {distanceString(leg.distance)} } - to {getPlaceName(leg.to, configCompanies)} -
- {!leg.hailedCar && ( -
- {leg.steps.map((step, k) => { - return ( -
- {getStepDirection(step)} on {getStepStreetName(step)} -
- ) - })} -
- )} -
-
- ) - } -} - -class TNCLeg extends Component { - static propTypes = { - leg: PropTypes.object - } - - render () { - const { customIcons, leg } = this.props - const { tncData } = leg - if (!tncData) return null - - return ( -
-
{getLegIcon(leg, customIcons)}
-
-
- Take {tncData.displayName} to {leg.to.name} -
-
-
- Estimated wait time for pickup:{' '} - {formatDuration(tncData.estimatedArrival)} -
-
- Estimated travel time:{' '} - {formatDuration(leg.duration)} (does not account for traffic) -
-
-
-
- ) - } -} diff --git a/lib/components/narrative/realtime-annotation.js b/lib/components/narrative/realtime-annotation.js index 834046d50..31ed7cd0e 100644 --- a/lib/components/narrative/realtime-annotation.js +++ b/lib/components/narrative/realtime-annotation.js @@ -1,9 +1,8 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button, OverlayTrigger, Popover } from 'react-bootstrap' -import { formatDuration } from '../../util/time' - export default class RealtimeAnnotation extends Component { static propTypes = { realtimeEffects: PropTypes.object, @@ -33,7 +32,7 @@ export default class RealtimeAnnotation extends Component { ? Your trip results have been adjusted based on real-time information. Under normal conditions, this trip would take{' '} - {formatDuration(realtimeEffects.normalDuration)} + {coreUtils.time.formatDuration(realtimeEffects.normalDuration)} using the following routes:{' '} {filteredRoutes .map((route, idx) => ( diff --git a/lib/components/narrative/tabbed-itineraries.js b/lib/components/narrative/tabbed-itineraries.js index f3c4e2d76..94f38eeb8 100644 --- a/lib/components/narrative/tabbed-itineraries.js +++ b/lib/components/narrative/tabbed-itineraries.js @@ -1,13 +1,15 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' import { setActiveItinerary, setActiveLeg, setActiveStep, setUseRealtimeResponse } from '../../actions/narrative' import DefaultItinerary from './default/default-itinerary' import { getActiveSearch, getRealtimeEffects } from '../../util/state' -import { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } from '../../util/itinerary' -import { formatDuration, formatTime, getTimeFormat } from '../../util/time' + +const { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } = coreUtils.itinerary +const { formatDuration, formatTime, getTimeFormat } = coreUtils.time class TabbedItineraries extends Component { static propTypes = { diff --git a/lib/components/narrative/trip-details.js b/lib/components/narrative/trip-details.js deleted file mode 100644 index 0b7118d4f..000000000 --- a/lib/components/narrative/trip-details.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import { Button } from 'react-bootstrap' -import { VelocityTransitionGroup } from 'velocity-react' -import moment from 'moment' - -import { calculateFares, calculatePhysicalActivity, getTimeZoneOffset } from '../../util/itinerary' -import { formatTime, getTimeFormat, getLongDateFormat } from '../../util/time' - -class TripDetails extends Component { - render () { - const { itinerary, timeFormat, longDateFormat } = this.props - const date = moment(itinerary.startTime) - - // process the transit fare - const { centsToString, dollarsToString, maxTNCFare, minTNCFare, transitFare } = calculateFares(itinerary) - let companies - itinerary.legs.forEach(leg => { - if (leg.tncData) { - companies = leg.tncData.company - } - }) - let fare - if (transitFare || minTNCFare) { - fare = ( - - {transitFare && ( - Transit Fare: {centsToString(transitFare)} - )} - {minTNCFare !== 0 && ( - -
- - {companies.toLowerCase()} - {' '} - Fare: {dollarsToString(minTNCFare)} - {dollarsToString(maxTNCFare)} -
- )} -
- ) - } - - // Compute calories burned. - const { bikeDuration, caloriesBurned, walkDuration } = calculatePhysicalActivity(itinerary) - - const timeOptions = { - format: timeFormat, - offset: getTimeZoneOffset(itinerary) - } - - return ( -
-
Trip Details
-
- } - summary={ - - Depart {date.format(longDateFormat)} - {this.props.routingType === 'ITINERARY' && at {formatTime(itinerary.startTime, timeOptions)}} - - } - /> - {fare && ( - } - summary={fare} - /> - )} - {caloriesBurned > 0 && ( - } - summary={Calories Burned: {Math.round(caloriesBurned)}} - description={ - - Calories burned is based on {Math.round(walkDuration / 60)} minute(s){' '} - spent walking and {Math.round(bikeDuration / 60)} minute(s){' '} - spent biking during this trip. Adapted from{' '} - - Dietary Guidelines for Americans 2005, page 16, Table 4 - . - - } - /> - )} -
-
- ) - } -} - -class TripDetail extends Component { - constructor (props) { - super(props) - this.state = { - expanded: false - } - } - - _toggle = () => this.state.expanded ? this._onHideClick() : this._onExpandClick() - - _onExpandClick = () => { - this.setState({ expanded: true }) - } - - _onHideClick = () => { - this.setState({ expanded: false }) - } - - render () { - const { icon, summary, description } = this.props - return ( -
-
{icon}
-
- {summary} - {description && ( - - )} - - {this.state.expanded && ( -
- - {description} -
- )} -
-
-
- ) - } -} - -// Connect main class to redux store - -const mapStateToProps = (state, ownProps) => { - return { - routingType: state.otp.currentQuery.routingType, - tnc: state.otp.tnc, - timeFormat: getTimeFormat(state.otp.config), - longDateFormat: getLongDateFormat(state.otp.config) - } -} - -export default connect(mapStateToProps)(TripDetails) diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index 60d8996dd..3cb3e6df5 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -1,3 +1,4 @@ +import coreUtils from '@opentripplanner/core-utils' import React, { Component, PureComponent } from 'react' import PropTypes from 'prop-types' import { Label, Button } from 'react-bootstrap' @@ -8,13 +9,12 @@ import Icon from '../narrative/icon' import { setMainPanelContent, setViewedRoute } from '../../actions/ui' import { findRoutes, findRoute } from '../../actions/api' -import { routeComparator } from '../../util/itinerary' -function operatorIndexForRoute (operators, route) { +function operatorIndexForRoute (transitOperators, route) { if (!route.agency) return 0 - const index = operators.findIndex(o => + const index = transitOperators.findIndex(o => o.id.toLowerCase() === route.agency.id.split(':')[0].toLowerCase()) - if (index !== -1 && typeof operators[index].order !== 'undefined') return operators[index].order + if (index !== -1 && typeof transitOperators[index].order !== 'undefined') return transitOperators[index].order else return 0 } @@ -52,17 +52,17 @@ class RouteViewer extends Component { findRoute, hideBackButton, languageConfig, - operators, + transitOperators, routes, setViewedRoute, viewedRoute } = this.props const sortedRoutes = routes - ? Object.values(routes).sort(routeComparator) + ? Object.values(routes).sort(coreUtils.itinerary.routeComparator) : [] - const agencySortedRoutes = operators.length > 0 + const agencySortedRoutes = transitOperators.length > 0 ? sortedRoutes.sort((a, b) => { - return operatorIndexForRoute(operators, a) - operatorIndexForRoute(operators, b) + return operatorIndexForRoute(transitOperators, a) - operatorIndexForRoute(transitOperators, b) }) : sortedRoutes return ( @@ -94,7 +94,7 @@ class RouteViewer extends Component { .map(route => { // Find operator based on agency_id (extracted from OTP route ID). // TODO: re-implement multi-agency logos for route viewer. - // const operator = operatorForRoute(operators, route) || {} + // const operator = operatorForRoute(transitOperators, route) || {} return ( {// TODO: re-implement multi-agency logos for route viewer. // Currently, the agency object is not nested within the get all - // routes endpoint and causing this to only display operators for + // routes endpoint and causing this to only display transitOperators for // the selected route. // operator && } @@ -196,7 +196,7 @@ class RouteRow extends PureComponent { const mapStateToProps = (state, ownProps) => { return { - operators: state.otp.config.operators, + transitOperators: state.otp.config.transitOperators, routes: state.otp.transitIndex.routes, viewedRoute: state.otp.ui.viewedRoute, languageConfig: state.otp.config.language diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index a1726dd1b..ac668f353 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -1,20 +1,25 @@ -import React, { Component } from 'react' +import moment from 'moment' +import 'moment-timezone' +import coreUtils from '@opentripplanner/core-utils' +import FromToLocationPicker from '@opentripplanner/from-to-location-picker' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import moment from 'moment' -import 'moment-timezone' import { VelocityTransitionGroup } from 'velocity-react' import Icon from '../narrative/icon' -import LocationIcon from '../icons/location-icon' - import { setMainPanelContent, toggleAutoRefresh } from '../../actions/ui' import { findStop, findStopTimesForStop } from '../../actions/api' import { forgetStop, rememberStop, setLocation } from '../../actions/map' -import { routeComparator } from '../../util/itinerary' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' -import { formatDuration, formatSecondsAfterMidnight, getTimeFormat, getUserTimezone } from '../../util/time' + +const { + formatDuration, + formatSecondsAfterMidnight, + getTimeFormat, + getUserTimezone +} = coreUtils.time class StopViewer extends Component { state = {} @@ -27,14 +32,14 @@ class StopViewer extends Component { _backClicked = () => this.props.setMainPanelContent(null) - _setLocationFromStop = (type) => { + _setLocationFromStop = (locationType) => { const { setLocation, stopData } = this.props const location = { name: stopData.name, lat: stopData.lat, lon: stopData.lon } - setLocation({ type, location, reverseGeocode: true }) + setLocation({ locationType, location, reverseGeocode: true }) this.setState({ popupPosition: null }) } @@ -217,24 +222,15 @@ class StopViewer extends Component { .format(timeFormat)}
- Plan a trip:{' '} - {' '} - {' '}|{' '} - {' '} - + Plan a trip: +
{/* pattern listing */} {stopData.stopTimes && stopData.routes && (
{Object.values(stopTimesByPattern) - .sort((a, b) => routeComparator(a.route, b.route)) + .sort((a, b) => coreUtils.itinerary.routeComparator(a.route, b.route)) .map(patternTimes => { // Only add pattern row if route is found. // FIXME: there is currently a bug with the alernative transit index diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index f1d3fe928..9ce414d29 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { Button, Label } from 'react-bootstrap' import { connect } from 'react-redux' @@ -10,8 +11,6 @@ import { setViewedTrip } from '../../actions/ui' import { findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' -import { formatSecondsAfterMidnight, getTimeFormat } from '../../util/time' - class TripViewer extends Component { static propTypes = { hideBackButton: PropTypes.bool, @@ -101,7 +100,7 @@ class TripViewer extends Component {
{/* the departure time */}
- {formatSecondsAfterMidnight(tripData.stopTimes[i].scheduledDeparture, timeFormat)} + {coreUtils.time.formatSecondsAfterMidnight(tripData.stopTimes[i].scheduledDeparture, timeFormat)}
{/* the vertical strip map */} @@ -137,7 +136,7 @@ const mapStateToProps = (state, ownProps) => { const viewedTrip = state.otp.ui.viewedTrip return { languageConfig: state.otp.config.language, - timeFormat: getTimeFormat(state.otp.config), + timeFormat: coreUtils.time.getTimeFormat(state.otp.config), tripData: state.otp.transitIndex.trips[viewedTrip.tripId], viewedTrip } diff --git a/lib/index.css b/lib/index.css index 41bd670a6..fa44435f1 100644 --- a/lib/index.css +++ b/lib/index.css @@ -4,15 +4,12 @@ @import url(node_modules/font-awesome/css/font-awesome.css); @import url(node_modules/react-dates/lib/css/_datepicker.css); @import url(node_modules/transitive-js/lib/transitive.css); -@import url(node_modules/leaflet.polylinemeasure/Leaflet.PolylineMeasure.css); @import url(lib/components/app/app.css); @import url(lib/components/map/map.css); @import url(lib/components/form/form.css); @import url(lib/components/narrative/narrative.css); @import url(lib/components/narrative/default/itinerary.css); -@import url(lib/components/narrative/line-itin/itinerary.css); -@import url(lib/components/narrative/printable/itinerary.css); @import url(lib/components/mobile/mobile.css); @import url(lib/components/viewers/viewers.css); @@ -51,11 +48,3 @@ button.header, button.step, .clear-button-formatting { button.header, button.step { width: 100%; } - -.otp .from-location-icon { - color: #333; -} - -.otp .to-location-icon { - color: #f44256; -} diff --git a/lib/index.js b/lib/index.js index bd8f7a849..4ed9f8b55 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,40 +1,27 @@ import DateTimeModal from './components/form/date-time-modal' import DateTimePreview from './components/form/date-time-preview' -import DateTimeSelector from './components/form/date-time-selector' import DefaultSearchForm from './components/form/default-search-form' import ErrorMessage from './components/form/error-message' -import GeneralSettingsPanel from './components/form/general-settings-panel' -import LocationField from './components/form/location-field' -import ModeSelector from './components/form/mode-selector' -import ModesPanel from './components/form/modes-panel' +import LocationField from './components/form/connected-location-field' import PlanTripButton from './components/form/plan-trip-button' import SettingsPreview from './components/form/settings-preview' -import SettingsSelectorPanel from './components/form/settings-selector-panel' import SwitchButton from './components/form/switch-button' -import LocationIcon from './components/icons/location-icon' - -import BaseMap from './components/map/base-map' import DefaultMap from './components/map/default-map' -import EndpointsOverlay from './components/map/endpoints-overlay' -import ItineraryOverlay from './components/map/itinerary-overlay' import Map from './components/map/map' -import TransitiveOverlay from './components/map/transitive-overlay' import StylizedMap from './components/map/stylized-map' import OsmBaseLayer from './components/map/osm-base-layer' -import StopsOverlay from './components/map/stops-overlay' import TileOverlay from './components/map/tile-overlay' import ItineraryCarousel from './components/narrative/itinerary-carousel' import LegDiagramPreview from './components/narrative/leg-diagram-preview' import NarrativeItineraries from './components/narrative/narrative-itineraries' -import NarrativeProfileOptions from './components/narrative/narrative-profile-options' import NarrativeItinerary from './components/narrative/narrative-itinerary' import NarrativeRoutingResults from './components/narrative/narrative-routing-results' import RealtimeAnnotation from './components/narrative/realtime-annotation' import SimpleRealtimeAnnotation from './components/narrative/simple-realtime-annotation' import TransportationNetworkCompanyLeg from './components/narrative/default/tnc-leg' -import TripDetails from './components/narrative/trip-details' +import TripDetails from './components/narrative/connected-trip-details' import TripTools from './components/narrative/trip-tools' import LineItinerary from './components/narrative/line-itin/line-itinerary' @@ -63,40 +50,26 @@ export { // form components DateTimeModal, DateTimePreview, - DateTimeSelector, DefaultSearchForm, ErrorMessage, - GeneralSettingsPanel, LocationField, - ModeSelector, - ModesPanel, PlanTripButton, SettingsPreview, - SettingsSelectorPanel, StylizedMap, SwitchButton, // map components - BaseMap, DefaultMap, - EndpointsOverlay, ItineraryCarousel, - ItineraryOverlay, Map, OsmBaseLayer, - StopsOverlay, TileOverlay, - TransitiveOverlay, - - // icon components - LocationIcon, // narrative components LegDiagramPreview, LineItinerary, NarrativeItineraries, NarrativeItinerary, - NarrativeProfileOptions, NarrativeRoutingResults, RealtimeAnnotation, SimpleRealtimeAnnotation, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 61ced6a35..d18fbdb20 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -2,18 +2,20 @@ import clone from 'clone' import update from 'immutability-helper' import isEqual from 'lodash.isequal' import objectPath from 'object-path' +import coreUtils from '@opentripplanner/core-utils' -import { matchLatLon } from '../util/map' -import { +import { MainPanelContent, MobileScreens } from '../actions/ui' + +const { isTransit, getTransitModes } = coreUtils.itinerary +const { matchLatLon } = coreUtils.map +const { filterProfileOptions } = coreUtils.profile +const { ensureSingleAccessMode, getDefaultQuery, getTripOptionsFromQuery -} from '../util/query' -import { isTransit, getTransitModes } from '../util/itinerary' -import { filterProfileOptions } from '../util/profile' -import { getItem, removeItem, storeItem } from '../util/storage' -import { getUserTimezone } from '../util/time' -import { MainPanelContent, MobileScreens } from '../actions/ui' +} = coreUtils.query +const { getItem, removeItem, storeItem } = coreUtils.storage +const { getUserTimezone } = coreUtils.time const MAX_RECENT_STORAGE = 5 @@ -64,7 +66,7 @@ export function getInitialState (userDefinedConfig, initialQuery) { autoPlan: false, debouncePlanTimeMs: 0, language: {}, - operators: [], + transitOperators: [], realtimeEffectsDisplayThreshold: 120, routingTypes: [], stopViewer: { @@ -251,7 +253,7 @@ function createOtpReducer (config, initialQuery) { } }, ui: { - diagramLeg: { $set: false } + diagramLeg: { $set: null } } }) case 'NON_REALTIME_ROUTING_RESPONSE': @@ -368,12 +370,12 @@ function createOtpReducer (config, initialQuery) { case 'SET_LOCATION': return update(state, { currentQuery: { - [action.payload.type]: { $set: action.payload.location } + [action.payload.locationType]: { $set: action.payload.location } } }) case 'CLEAR_LOCATION': return update(state, { - currentQuery: { [action.payload.type]: { $set: null } } + currentQuery: { [action.payload.locationType]: { $set: null } } }) case 'SET_QUERY_PARAM': diff --git a/lib/util/distance.js b/lib/util/distance.js deleted file mode 100644 index e63c8a0fe..000000000 --- a/lib/util/distance.js +++ /dev/null @@ -1,26 +0,0 @@ -export function distanceStringImperial (meters, abbreviate) { - const feet = meters * 3.28084 - if (feet < 528) return Math.round(feet) + (abbreviate === true ? ' ft' : ' feet') - return (Math.round(feet / 528) / 10) + (abbreviate === true ? ' mi' : ' miles') -} - -export function distanceStringMetric (meters) { - let km = meters / 1000 - if (km > 100) { - // 100 km => 999999999 km - km = km.toFixed(0) - return km + ' km' - } else if (km > 1) { - // 1.1 km => 99.9 km - km = km.toFixed(1) - return km + ' km' - } else { - // 1m => 999m - meters = meters.toFixed(0) - return meters + ' m' - } -} - -export function distanceString (meters, outputMetricUntis = false) { - return (outputMetricUntis === true) ? distanceStringMetric(meters) : distanceStringImperial(meters) -} diff --git a/lib/util/geocoder.js b/lib/util/geocoder.js deleted file mode 100644 index c6ac8d6cf..000000000 --- a/lib/util/geocoder.js +++ /dev/null @@ -1,318 +0,0 @@ -import * as arcgis from '@conveyal/geocoder-arcgis-geojson' -import lonlat from '@conveyal/lonlat' -import * as pelias from 'isomorphic-mapzen-search' -import memoize from 'lodash.memoize' - -/** - * Create customized geocoder functions given a certain geocoding API, the - * config for the geocoder and response rewrite functions specific to this - * application. Any geocoder api that is added is expected to have an API that - * behaves very closely to https://github.com/conveyal/isomorphic-mapzen-search - */ -class Geocoder { - constructor (geocoderApi, geocoderConfig) { - this.api = geocoderApi - this.geocoderConfig = geocoderConfig - } - - /** - * Perform an autocomplete query. Eg, using partial text of a possible - * address or POI, attempt to find possible matches. - */ - autocomplete (query) { - return this.api.autocomplete(this.getAutocompleteQuery(query)) - .then(this.rewriteAutocompleteResponse) - } - - /** - * Get an application-specific data structure from a given feature. The - * feature is either the result of an autocomplete or a search query. This - * function returns a Promise because sometimes an asynchronous action - * needs to be taken to translate a feature into a location. For example, - * the ArcGIS autocomplete service returns results that lack full address - * data and GPS and it is expected that an extra call to the `search` API is - * done to obtain that detailed data. - */ - getLocationFromGeocodedFeature (feature) { - const location = lonlat.fromCoordinates(feature.geometry.coordinates) - location.name = feature.properties.label - return Promise.resolve(location) - } - - /** - * Do a reverse-geocode. ie get address information and attributes given a - * GPS coordiante. - */ - reverse (query) { - return this.api.reverse(this.getReverseQuery(query)) - .then(this.rewriteReverseResponse) - } - - /** - * Perform a search query. A search query is different from autocomplete in - * that it is assumed that the text provided is more or less a complete - * well-fromatted address. - */ - search (query) { - return this.api.search(this.getSearchQuery(query)) - .then(this.rewriteSearchResponse) - } - - /** - * Default autocomplete query generator - */ - getAutocompleteQuery (query) { - const {apiKey, baseUrl, boundary, options, focusPoint} = this.geocoderConfig - return { - apiKey, - boundary, - focusPoint, - options, - url: baseUrl ? `${baseUrl}/autocomplete` : undefined, - ...query - } - } - - /** - * Default reverse query generator - */ - getReverseQuery (query) { - const {apiKey, baseUrl, options} = this.geocoderConfig - return { - apiKey, - format: true, - options, - url: baseUrl ? `${baseUrl}/reverse` : undefined, - ...query - } - } - - /** - * Default search query generator. - */ - getSearchQuery (query) { - const {apiKey, baseUrl, boundary, focusPoint, options} = this.geocoderConfig - return { - apiKey, - boundary, - focusPoint, - options, - url: baseUrl ? `${baseUrl}/search` : undefined, - format: false, // keep as returned GeoJSON, - ...query - } - } - - /** - * Default rewriter for autocomplete responses - */ - rewriteAutocompleteResponse (response) { return response } - - /** - * Default rewriter for reverse responses - */ - rewriteReverseResponse (response) { return response } - - /** - * Default rewriter for search responses - */ - rewriteSearchResponse (response) { return response } -} - -/** - * Geocoder implementation for the ArcGIS geocoder. - * See https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm - * - * @extends Geocoder - */ -class ArcGISGeocoder extends Geocoder { - /** - * Using the given magicKey and text, perform a search query to get detailed - * address and GPS data. Return data in an application-specific location - * format. - */ - getLocationFromGeocodedFeature (feature) { - return this.api.search({ magicKey: feature.magicKey, text: feature.text }) - .then(response => { - const feature = response.features[0] - const location = lonlat.fromCoordinates(feature.geometry.coordinates) - location.name = feature.properties.label - return location - }) - } - - /** - * Rewrite an autocomplete response into an application specific data format. - * Also, filter out any results that are collections. - */ - rewriteAutocompleteResponse (response) { - return { - // remove any autocomplete results that are collections - // (eg multiple Starbucks) - features: response.features.filter(feature => !feature.isCollection) - // add label property so location-field can handle things ok - .map(feature => ({ - ...feature, - properties: { - label: feature.text - } - })) - } - } - - /** - * Rewrite the response into an application-specific data format using the - * first feature returned from the geocoder. - */ - rewriteReverseResponse (response) { - const { features, query } = response - const { lat, lon } = query - return { - lat, - lon, - name: features[0].properties.label - } - } -} - -/** - * An implementation that doesn't use an API for geocoding. Merely allows - * clicking on the map and finding GPS coordinates by typing them in. - * - * @extends Geocoder - */ -class NoApiGeocoder extends Geocoder { - /** - * Use coordinate string parser. - */ - autocomplete (query) { - return this.parseCoordinateString(query.text) - } - - /** - * Always return the lat/lon. - */ - reverse (query) { - let { lat, lon } = query.point - lat = this.roundGPSDecimal(lat) - lon = this.roundGPSDecimal(lon) - return Promise.resolve({ lat, lon, name: `${lat}, ${lon}` }) - } - - /** - * Use coordinate string parser. - */ - search (query) { - return this.parseCoordinateString(query.text) - } - - /** - * Attempt to parse the input as a GPS coordinate. If parseable, return a - * feature. - */ - parseCoordinateString (string) { - let feature - try { - feature = { - geometry: { - coordinates: lonlat.toCoordinates(lonlat.fromLatFirstString(string)), - type: 'Point' - }, - properties: { - label: string - } - } - } catch (e) { - return Promise.resolve({ features: [] }) - } - return Promise.resolve({ features: [feature] }) - } - - roundGPSDecimal (number) { - const roundFactor = 100000 - return Math.round(number * roundFactor) / roundFactor - } -} - -/** - * Geocoder implementation for the Pelias geocoder. - * See https://pelias.io - * - * This is exported for testing purposes only. - * - * @extends Geocoder - */ -export class PeliasGeocoder extends Geocoder { - /** - * Generate an autocomplete query specifically for the Pelias API. The - * `sources` parameter is a Pelias-specific option. - */ - getAutocompleteQuery (query) { - const {apiKey, baseUrl, boundary, focusPoint, options, sources} = this.geocoderConfig - return { - apiKey, - boundary, - focusPoint, - options, - // explicitly send over null for sources if provided sources is not truthy - // in order to avoid default isomorphic-mapzen-search sources form being - // applied - sources: sources || null, - url: baseUrl ? `${baseUrl}/autocomplete` : undefined, - ...query - } - } - - /** - * Generate a search query specifically for the Pelias API. The - * `sources` parameter is a Pelias-specific option. - */ - getSearchQuery (query) { - const {apiKey, baseUrl, boundary, focusPoint, options, sources} = this.geocoderConfig - return { - apiKey, - boundary, - focusPoint, - // explicitly send over null for sources if provided sources is not truthy - // in order to avoid default isomorphic-mapzen-search sources form being - // applied - options, - sources: sources || null, - url: baseUrl ? `${baseUrl}/search` : undefined, - format: false, // keep as returned GeoJSON, - ...query - } - } - - /** - * Rewrite the response into an application-specific data format using the - * first feature returned from the geocoder. - */ - rewriteReverseResponse (response) { - const { 'point.lat': lat, 'point.lon': lon } = response.isomorphicMapzenSearchQuery - return { - lat, - lon, - name: response[0].address - } - } -} - -// Create a memoized getter to avoid recreating new geocoders each time. -const getGeocoder = memoize(geocoderConfig => { - if (!geocoderConfig || !geocoderConfig.type) { - return new NoApiGeocoder() - } - const {type} = geocoderConfig - switch (type) { - case 'ARCGIS': - return new ArcGISGeocoder(arcgis, geocoderConfig) - case 'PELIAS': - return new PeliasGeocoder(pelias, geocoderConfig) - default: - console.error(`Unkown geocoder type: "${type}". Using NoApiGeocoder.`) - return new NoApiGeocoder() - } -}) - -export default getGeocoder diff --git a/lib/util/index.js b/lib/util/index.js index b04bbc761..02d4e6327 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -1,25 +1,9 @@ -import * as distance from './distance' -import getGeocoder from './geocoder' import * as itinerary from './itinerary' -import * as map from './map' -import * as profile from './profile' -import * as query from './query' -import * as reverse from './reverse' import * as state from './state' -import * as time from './time' -import * as ui from './ui' const OtpUtils = { - distance, - getGeocoder, itinerary, - map, - profile, - query, - reverse, - state, - time, - ui + state } export default OtpUtils diff --git a/lib/util/itinerary.js b/lib/util/itinerary.js index 56d98c658..70468dbe5 100644 --- a/lib/util/itinerary.js +++ b/lib/util/itinerary.js @@ -1,766 +1,13 @@ -import React from 'react' import { latLngBounds } from 'leaflet' -import polyline from '@mapbox/polyline' -import turfAlong from 'turf-along' +import coreUtils from '@opentripplanner/core-utils' -import ModeIcon from '../components/icons/mode-icon' - -// All OTP transit modes -export const transitModes = ['TRAM', 'BUS', 'SUBWAY', 'FERRY', 'RAIL', 'GONDOLA'] - -/** - * @param {config} config OTP-RR configuration object - * @return {Array} List of all transit modes defined in config; otherwise default mode list - */ - -export function getTransitModes (config) { - if (!config || !config.modes || !config.modes.transitModes) return transitModes - return config.modes.transitModes.map(tm => tm.mode) -} - -export function isTransit (mode) { - return transitModes.includes(mode) || mode === 'TRANSIT' -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are transit modes - */ -export function hasTransit (modesStr) { - for (const mode of modesStr.split(',')) { - if (isTransit(mode)) return true - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are car-based modes - */ -export function hasCar (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isCar(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are bicycle-based modes - */ -export function hasBike (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isBicycle(mode) || isBicycleRent(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes are micromobility-based modes - */ -export function hasMicromobility (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (isMicromobility(mode)) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes is a hailing mode - */ -export function hasHail (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (mode.indexOf('_HAIL') > -1) return true - } - } - return false -} - -/** - * @param {string} modesStr a comma-separated list of OTP modes - * @return {boolean} whether any of the modes is a rental mode - */ -export function hasRental (modesStr) { - if (modesStr) { - for (const mode of modesStr.split(',')) { - if (mode.indexOf('_RENT') > -1) return true - } - } - return false -} - -export function isWalk (mode) { - if (!mode) return false - - return mode === 'WALK' -} - -export function isBicycle (mode) { - if (!mode) return false - - return mode === 'BICYCLE' -} - -export function isBicycleRent (mode) { - if (!mode) return false - - return mode === 'BICYCLE_RENT' -} - -export function isCar (mode) { - if (!mode) return false - return mode.startsWith('CAR') -} - -export function isMicromobility (mode) { - if (!mode) return false - return mode.startsWith('MICROMOBILITY') -} - -export function isAccessMode (mode) { - return isWalk(mode) || - isBicycle(mode) || - isBicycleRent(mode) || - isCar(mode) || - isMicromobility(mode) -} - -export function getMapColor (mode) { - mode = mode || this.get('mode') - if (mode === 'WALK') return '#444' - if (mode === 'BICYCLE') return '#0073e5' - if (mode === 'SUBWAY') return '#f00' - if (mode === 'RAIL') return '#b00' - if (mode === 'BUS') return '#080' - if (mode === 'TRAM') return '#800' - if (mode === 'FERRY') return '#008' - if (mode === 'CAR') return '#444' - if (mode === 'MICROMOBILITY') return '#f5a729' - return '#aaa' -} - -// TODO: temporary code; handle via migrated OTP i18n language table -export function getStepDirection (step) { - switch (step.relativeDirection) { - case 'DEPART': return 'Head ' + step.absoluteDirection.toLowerCase() - case 'LEFT': return 'Left' - case 'HARD_LEFT': return 'Hard left' - case 'SLIGHTLY_LEFT': return 'Slight left' - case 'CONTINUE': return 'Continue' - case 'SLIGHTLY_RIGHT': return 'Slight right' - case 'RIGHT': return 'Right' - case 'HARD_RIGHT': return 'Hard right' - case 'CIRCLE_CLOCKWISE': return 'Follow circle clockwise' - case 'CIRCLE_COUNTERCLOCKWISE': return 'Follow circle counterclockwise' - case 'ELEVATOR': return 'Take elevator' - case 'UTURN_LEFT': return 'Left U-turn' - case 'UTURN_RIGHT': return 'Right U-turn' - } - return step.relativeDirection -} - -export function getStepInstructions (step) { - const conjunction = step.relativeDirection === 'ELEVATOR' ? 'to' : 'on' - return `${getStepDirection(step)} ${conjunction} ${step.streetName}` -} - -export function getStepStreetName (step) { - if (step.streetName === 'road') return 'Unnamed Road' - if (step.streetName === 'path') return 'Unnamed Path' - return step.streetName -} - -export function getLegModeLabel (leg) { - switch (leg.mode) { - case 'BICYCLE_RENT': return 'Biketown' - case 'CAR': return leg.hailedCar ? 'Ride' : 'Drive' - case 'GONDOLA': return 'Aerial Tram' - case 'TRAM': - if (leg.routeLongName.toLowerCase().indexOf('streetcar') !== -1) return 'Streetcar' - return 'Light Rail' - case 'MICROMOBILITY': return 'Ride' - } - return toSentenceCase(leg.mode) -} - -/** - * Returns a react element of the desired icon. If customIcons are defined, then - * the icon will be attempted to be used from that lookup of icons. Otherwise, - * a ModeIcon element will be returned. - * - * @param {string} iconId A string with the desired icon ID. This icon can - * include modes or companies or anything that is defined in the customIcons. - * @param {[Map]} customIcons A customized lookup of - * icons. These are defined as part of the implementing webapp. If this lookup - * is not defined, then the ModeIcon class will be used instead. - * @return {React.Element} - */ -export function getIcon (iconId, customIcons) { - // Check if there is a custom icon - if (customIcons && iconId in customIcons) { - return customIcons[iconId] - } - - // Custom icon not available for the given iconId. Use the ModeIcon component - // to show the icon based on the iconId, but always use the default car icon - // for any car-based modes that didn't have custom icon - if (iconId && iconId.startsWith('CAR')) iconId = 'CAR' - return -} - -export function getItineraryBounds (itinerary) { - let coords = [] - itinerary.legs.forEach(leg => { - const legCoords = polyline - .toGeoJSON(leg.legGeometry.points) - .coordinates.map(c => [c[1], c[0]]) - coords = [...coords, ...legCoords] - }) - return latLngBounds(coords) +export function getLeafletItineraryBounds (itinerary) { + return latLngBounds(coreUtils.itinerary.getItineraryBounds(itinerary)) } /** * Return a leaflet LatLngBounds object that encloses the given leg's geometry. */ -export function getLegBounds (leg) { - const coords = polyline - .toGeoJSON(leg.legGeometry.points) - .coordinates.map(c => [c[1], c[0]]) - - // in certain cases, there might be zero-length coordinates in the leg - // geometry. In these cases, build us an array of coordinates using the from - // and to data of the leg. - if (coords.length === 0) { - coords.push([leg.from.lat, leg.from.lon], [leg.to.lat, leg.to.lon]) - } - return latLngBounds(coords) -} - -/** - * Gets the desired sort values according to an optional getter function. If the - * getter function is not defined, the original sort values are returned. - */ -function getSortValues (getterFn, a, b) { - let aVal - let bVal - if (typeof getterFn === 'function') { - aVal = getterFn(a) - bVal = getterFn(b) - } else { - aVal = a - bVal = b - } - return { aVal, bVal } -} - -// Lookup for the sort values associated with various OTP modes. -// Note: JSDoc format not used to avoid bug in documentationjs. -// https://github.com/documentationjs/documentation/issues/372 -const modeComparatorValue = { - SUBWAY: 1, - TRAM: 2, - RAIL: 3, - GONDOLA: 4, - FERRY: 5, - CABLE_CAR: 6, - FUNICULAR: 7, - BUS: 8 -} - -// Lookup that maps route types to the OTP mode sort values. -// Note: JSDoc format not used to avoid bug in documentationjs. -// https://github.com/documentationjs/documentation/issues/372 -const routeTypeComparatorValue = { - 0: modeComparatorValue.TRAM, // - Tram, Streetcar, Light rail. - 1: modeComparatorValue.SUBWAY, // - Subway, Metro. - 2: modeComparatorValue.RAIL, // - Rail. Used for intercity or long-distance travel. - 3: modeComparatorValue.BUS, // - Bus. - 4: modeComparatorValue.FERRY, // - Ferry. - 5: modeComparatorValue.CABLE_CAR, // - Cable tram. - 6: modeComparatorValue.GONDOLA, // - Gondola, etc. - 7: modeComparatorValue.FUNICULAR, // - Funicular. - // TODO: 11 and 12 are not a part of OTP as of 2019-02-14, but for now just - // associate them with bus/rail. - 11: modeComparatorValue.BUS, // - Trolleybus. - 12: modeComparatorValue.RAIL // - Monorail. -} - -// Gets a comparator value for a given route's type (OTP mode). -// Note: JSDoc format not used to avoid bug in documentationjs. -// ttps://github.com/documentationjs/documentation/issues/372 -function getRouteTypeComparatorValue (route) { - // For some strange reason, the short route response in OTP returns the - // string-based modes, but the long route response returns the - // integer route type. This attempts to account for both of those cases. - if (!route) throw new Error('Route is undefined.', route) - if (typeof modeComparatorValue[route.mode] !== 'undefined') { - return modeComparatorValue[route.mode] - } else if (typeof routeTypeComparatorValue[route.type] !== 'undefined') { - return routeTypeComparatorValue[route.type] - } else { - // Default the comparator value to a large number (placing the route at the - // end of the list). - console.warn('no mode/route type found for route', route) - return 9999 - } -} - -/** - * Calculates the sort comparator value given two routes based off of route type - * (OTP mode). - */ -function routeTypeComparator (a, b) { - return getRouteTypeComparatorValue(a) - getRouteTypeComparatorValue(b) -} - -/** - * Determines whether a value is a string that starts with an alphabetic - * ascii character. - */ -function startsWithAlphabeticCharacter (val) { - if (typeof val === 'string' && val.length > 0) { - const firstCharCode = val.charCodeAt(0) - return (firstCharCode >= 65 && firstCharCode <= 90) || - (firstCharCode >= 97 && firstCharCode <= 122) - } - return false -} - -/** - * Sorts routes based off of whether the shortName begins with an alphabetic - * character. Routes with shortn that do start with an alphabetic character will - * be prioritized over those that don't. - */ -function alphabeticShortNameComparator (a, b) { - const aStartsWithAlphabeticCharacter = startsWithAlphabeticCharacter( - a.shortName - ) - const bStartsWithAlphabeticCharacter = startsWithAlphabeticCharacter( - b.shortName - ) - - if (aStartsWithAlphabeticCharacter && bStartsWithAlphabeticCharacter) { - // both start with an alphabetic character, return equivalence - return 0 - } - // a does start with an alphabetic character, but b does not. Prioritize a - if (aStartsWithAlphabeticCharacter) return -1 - // b does start with an alphabetic character, but a does not. Prioritize b - if (bStartsWithAlphabeticCharacter) return 1 - // neither route has a shortName that starts with an alphabetic character. - // Return equivalence - return 0 -} - -/** - * Checks whether an appropriate comparison of numeric values can be made for - * sorting purposes. If both values are not valid numbers according to the - * isNaN check, then this function returns undefined which indicates that a - * secondary sorting criteria should be used instead. If one value is valid and - * the other is not, then the valid value will be given sorting priority. If - * both values are valid numbers, the difference is obtained as the sort value. - * - * An optional argument can be provided which will be used to obtain the - * comparison value from the comparison function arguments. - * - * IMPORTANT: the comparison values must be numeric values or at least be - * attempted to be converted to numeric values! If one of the arguments is - * something crazy like an empty string, unexpected behavior will occur because - * JavaScript. - * - * @param {function} [objGetterFn] An optional function to obtain the - * comparison value from the comparator function arguments - */ -function makeNumericValueComparator (objGetterFn) { - return (a, b) => { - const { aVal, bVal } = getSortValues(objGetterFn, a, b) - // if both values aren't valid numbers, use the next sort criteria - if (isNaN(aVal) && isNaN(bVal)) return 0 - // b is a valid number, b gets priority - if (isNaN(aVal)) return 1 - // a is a valid number, a gets priority - if (isNaN(bVal)) return -1 - // a and b are valid numbers, return the sort value - return aVal - bVal - } -} - -/** - * Create a comparator function that compares string values. The comparison - * values feed to the sort comparator function are assumed to be objects that - * will have either undefined, null or string values at the given key. If one - * object has undefined, null or an empty string, but the other does have a - * string with length > 0, then that string will get priority. - * - * @param {function} [objGetterFn] An optional function to obtain the - * comparison value from the comparator function arguments - */ -function makeStringValueComparator (objGetterFn) { - return (a, b) => { - const { aVal, bVal } = getSortValues(objGetterFn, a, b) - // both a and b are uncomparable strings, return equivalent value - if (!aVal && !bVal) return 0 - // a is not a comparable string, b gets priority - if (!aVal) return 1 - // b is not a comparable string, a gets priority - if (!bVal) return -1 - // a and b are comparable strings, return the sort value - if (aVal < bVal) return -1 - if (aVal > bVal) return 1 - return 0 - } -} - -/** - * OpenTripPlanner sets the routeSortOrder to -999 by default. So, if that value - * is encountered, assume that it actually means that the routeSortOrder is not - * set in the GTFS. - * - * See https://github.com/opentripplanner/OpenTripPlanner/issues/2938 - * Also see https://github.com/opentripplanner/otp-react-redux/issues/122 - */ -function getRouteSortOrderValue (val) { - return val === -999 ? undefined : val -} - -/** - * Create a multi-criteria sort comparator function composed of other sort - * comparator functions. Each comparator function will be ran in the order given - * until a non-zero comparison value is obtained which is then immediately - * returned. If all comparison functions return equivalance, then the values - * are assumed to be equivalent. - */ -function makeMultiCriteriaSort (...criteria) { - return (a, b) => { - for (let i = 0; i < criteria.length; i++) { - const curCriteriaComparatorValue = criteria[i](a, b) - // if the comparison objects are not equivalent, return the value obtained - // in this current criteria comparison - if (curCriteriaComparatorValue !== 0) { - return curCriteriaComparatorValue - } - } - return 0 - } -} - -/** - * Compares routes for the purposes of sorting and displaying in a user - * interface. Due to GTFS feeds having varying levels of data quality, a multi- - * criteria sort is needed to account for various differences. The criteria - * included here are each applied to the routes in the order listed. If a given - * sort criterion yields equivalence (e.g., two routes have the short name - * "20"), the comparator falls back onto the next sort criterion (e.g., long - * name). If desired, the criteria of sorting based off of integer shortName can - * be disabled. The sort operates on the following values (in order): - * - * 1. sortOrder. Routes that do not have a valid sortOrder will be placed - * beneath those that do. - * 2. route type (OTP mode). See routeTypeComparator code for prioritization of - * route types. - * 3. shortNames that begin with alphabetic characters. shortNames that do not - * start with alphabetic characters will be place beneath those that do. - * 4. shortName as integer. shortNames that cannot be parsed as integers will - * be placed beneath those that are valid. - * 5. shortName as string. Routes without shortNames will be placed beneath - * those with shortNames. - * 6. longName as string. - */ -export const routeComparator = makeMultiCriteriaSort( - makeNumericValueComparator(obj => getRouteSortOrderValue(obj.sortOrder)), - routeTypeComparator, - alphabeticShortNameComparator, - makeNumericValueComparator(obj => parseInt(obj.shortName)), - makeStringValueComparator(obj => obj.shortName), - makeStringValueComparator(obj => obj.longName) -) - -/* Returns an interpolated lat-lon at a specified distance along a leg */ - -export function legLocationAtDistance (leg, distance) { - if (!leg.legGeometry) return null - - try { - const line = polyline.toGeoJSON(leg.legGeometry.points) - const pt = turfAlong(line, distance, 'meters') - if (pt && pt.geometry && pt.geometry.coordinates) { - return [ - pt.geometry.coordinates[1], - pt.geometry.coordinates[0] - ] - } - } catch (e) { } - - return null -} - -/* Returns an interpolated elevation at a specified distance along a leg */ - -export function legElevationAtDistance (points, distance) { - // Iterate through the combined elevation profile - let traversed = 0 - // If first point distance is not zero, insert starting point at zero with - // null elevation. Encountering this value should trigger the warning below. - if (points[0][0] > 0) { - points.unshift([0, null]) - } - for (let i = 1; i < points.length; i++) { - const start = points[i - 1] - const elevDistanceSpan = points[i][0] - start[0] - if (distance >= traversed && distance <= traversed + elevDistanceSpan) { - // Distance falls within this point and the previous one; - // compute & return iterpolated elevation value - if (start[1] === null) { - console.warn('Elevation value does not exist for distance.', distance, traversed) - return null - } - const pct = (distance - traversed) / elevDistanceSpan - const elevSpan = points[i][1] - start[1] - return start[1] + elevSpan * pct - } - traversed += elevDistanceSpan - } - console.warn('Elevation value does not exist for distance.', distance, traversed) - return null -} - -// Iterate through the steps, building the array of elevation points and -// keeping track of the minimum and maximum elevations reached -export function getElevationProfile (steps, unitConversion = 1) { - let minElev = 100000 - let maxElev = -100000 - let traversed = 0 - let gain = 0 - let loss = 0 - let previous = null - const points = [] - steps.forEach((step, stepIndex) => { - if (!step.elevation || step.elevation.length === 0) { - traversed += step.distance - return - } - for (let i = 0; i < step.elevation.length; i++) { - const elev = step.elevation[i] - if (previous) { - const diff = (elev.second - previous.second) * unitConversion - if (diff > 0) gain += diff - else loss += diff - } - if (i === 0 && elev.first !== 0) { - // console.warn(`No elevation data available for step ${stepIndex}-${i} at beginning of segment`, elev) - } - const convertedElevation = elev.second * unitConversion - if (convertedElevation < minElev) minElev = convertedElevation - if (convertedElevation > maxElev) maxElev = convertedElevation - points.push([traversed + elev.first, elev.second]) - // Insert "filler" point if the last point in elevation profile does not - // reach the full distance of the step. - if (i === step.elevation.length - 1 && elev.first !== step.distance) { - // points.push([traversed + step.distance, elev.second]) - } - previous = elev - } - traversed += step.distance - }) - return { maxElev, minElev, points, traversed, gain, loss } -} - -/** - * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. - * - * @param {string} text The text to be rendered. - * @param {string} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). - * - * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 - */ -export function getTextWidth (text, font = '22px Arial') { - // re-use canvas object for better performance - var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas')) - var context = canvas.getContext('2d') - context.font = font - var metrics = context.measureText(text) - return metrics.width -} - -export function toSentenceCase (str) { - if (str == null) { - return '' - } - str = String(str) - return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase() -} - -/** - * Return an icon depending on the leg info - * - * @param {Object} leg The leg data of an itinerary in an OTP trip plan result - * @param {[Object]} customIcons If defined for this webapp, the custom icons - * consist of a lookup table of icons to return for a specific icon ID. These - * icons typically show either companies or transport modes, but they could show - * other icons too. See this file in trimet-mod-otp for an example setup: - * https://github.com/ibi-group/trimet-mod-otp/blob/6a32e2142655c4f4d09a3f349b971b7505e2866a/lib/icons/index.js#L24-L55 - */ -export function getLegIcon (leg, customIcons) { - // check if a custom function exists for determining the icon for a leg - if (customIcons && typeof customIcons.customIconForLeg === 'function') { - // function exits, get the icon string lookup. It's possible for there to be - // a custom function that only returns a string for when a leg meets the - // criteria of the custom function - const customIconStr = customIcons.customIconForLeg(leg) - // the customIconStr could be undefined for this leg, but if it is not, then - // immediately return this custom icon for the leg - if (customIconStr) return getIcon(customIconStr, customIcons) - } - let iconStr = leg.mode - if (iconStr === 'CAR' && leg.rentedCar) { - iconStr = leg.from.networks[0] - } else if (iconStr === 'CAR' && leg.tncData) { - iconStr = leg.tncData.company - } else if (iconStr === 'BICYCLE' && leg.rentedBike && leg.from.networks) { - iconStr = leg.from.networks[0] - } else if (iconStr === 'MICROMOBILITY' && leg.rentedVehicle && leg.from.networks) { - iconStr = leg.from.networks[0] - } - - return getIcon(iconStr, customIcons) -} - -/** - * Get the configured company object for the given network string if the company - * has been defined in the provided companies array config. - */ -function getCompanyForNetwork (networkString, companies = []) { - const company = companies.find(co => co.id === networkString) - if (!company) { - console.warn(`No company found in config.yml that matches rented vehicle network: ${networkString}`, companies) - } - return company -} - -/** - * Get a string label to display from a list of vehicle rental networks. - * - * @param {Array} networks A list of network ids. - * @param {Array} [companies=[]] An optional list of the companies config. - * @return {string} A label for use in presentation on a website. - */ -export function getCompaniesLabelFromNetworks (networks, companies = []) { - return networks.map(network => getCompanyForNetwork(network, companies)) - .filter(co => !!co) - .map(co => co.label) - .join('/') -} - -/** - * Returns mode name by checking the vertex type (VertexType class in OTP) for - * the provided place. NOTE: this is currently only intended for vehicles at - * the moment (not transit or walking). - * - * TODO: I18N - * @param {string} place place from itinerary leg - */ -export function getModeForPlace (place) { - switch (place.vertexType) { - case 'CARSHARE': - return 'car' - case 'VEHICLERENTAL': - return 'E-scooter' - // TODO: Should the type change depending on bike vertex type? - case 'BIKESHARE': - case 'BIKEPARK': - return 'bike' - // If company offers more than one mode, default to `vehicle` string. - default: - return 'vehicle' - } -} - -export function getPlaceName (place, companies) { - // If address is provided (i.e. for carshare station, use it) - if (place.address) return place.address.split(',')[0] - if (place.networks && place.vertexType === 'VEHICLERENTAL') { - // For vehicle rental pick up, do not use the place name. Rather, use - // company name + vehicle type (e.g., SPIN E-scooter). Place name is often just - // a UUID that has no relevance to the actual vehicle. For bikeshare, however, - // there are often hubs or bikes that have relevant names to the user. - const company = getCompanyForNetwork(place.networks[0], companies) - if (company) { - return `${company.label} ${getModeForPlace(place)}` - } - } - // Default to place name - return place.name -} - -export function getTNCLocation (leg, type) { - const location = leg[type] - return `${location.lat.toFixed(5)},${location.lon.toFixed(5)}` -} - -export function calculatePhysicalActivity (itinerary) { - let walkDuration = 0 - let bikeDuration = 0 - for (const leg of itinerary.legs) { - if (leg.mode.startsWith('WALK')) walkDuration += leg.duration - if (leg.mode.startsWith('BICYCLE')) bikeDuration += leg.duration - } - const caloriesBurned = - walkDuration / 3600 * 280 + - bikeDuration / 3600 * 290 - return { - bikeDuration, - caloriesBurned, - walkDuration - } -} - -export function calculateFares (itinerary) { - let transitFare = 0 - let symbol = '$' // default to USD - let dollarsToString = dollars => `${symbol}${dollars.toFixed(2)}` - let centsToString = cents => `${symbol}${(cents / Math.pow(10, 2)).toFixed(2)}` - if (itinerary.fare && itinerary.fare.fare && itinerary.fare.fare.regular) { - const reg = itinerary.fare.fare.regular - symbol = reg.currency.symbol - transitFare = reg.cents - centsToString = cents => `${symbol}${(cents / Math.pow(10, reg.currency.defaultFractionDigits)).toFixed(reg.currency.defaultFractionDigits)}` - dollarsToString = dollars => `${symbol}${dollars.toFixed(2)}` - } - - // Process any TNC fares - let minTNCFare = 0 - let maxTNCFare = 0 - for (const leg of itinerary.legs) { - if (leg.mode === 'CAR' && leg.hailedCar && leg.tncData) { - const { maxCost, minCost } = leg.tncData - // TODO: Support non-USD - minTNCFare += minCost - maxTNCFare += maxCost - } - } - return { - centsToString, - dollarsToString, - maxTNCFare, - minTNCFare, - transitFare - } -} - -export function getTimeZoneOffset (itinerary) { - if (!itinerary.legs || !itinerary.legs.length) return 0 - - // Determine if there is a DST offset between now and the itinerary start date - const dstOffset = new Date(itinerary.startTime).getTimezoneOffset() - new Date().getTimezoneOffset() - - return itinerary.legs[0].agencyTimeZoneOffset + (new Date().getTimezoneOffset() + dstOffset) * 60000 +export function getLeafletLegBounds (leg) { + return latLngBounds(coreUtils.itinerary.getLegBounds(leg)) } diff --git a/lib/util/map.js b/lib/util/map.js deleted file mode 100644 index 8c5d44db7..000000000 --- a/lib/util/map.js +++ /dev/null @@ -1,223 +0,0 @@ -import moment from 'moment' - -import { isTransit, toSentenceCase } from './itinerary' - -export function latlngToString (latlng) { - return latlng && `${latlng.lat.toFixed(5)}, ${(latlng.lng || latlng.lon).toFixed(5)}` -} - -export function coordsToString (coords) { - return coords.length && coords.map(c => (+c).toFixed(5)).join(', ') -} - -export function stringToCoords (str) { - return (str && str.split(',').map(c => +c)) || [] -} - -export function constructLocation (latlng) { - return { - name: latlngToString(latlng), - lat: latlng.lat, - lon: latlng.lng - } -} - -export function formatStoredPlaceName (location, withDetails = true) { - let displayName = location.type === 'home' || location.type === 'work' - ? toSentenceCase(location.type) - : location.name - if (withDetails) { - let detailText = getDetailText(location) - if (detailText) displayName += ` (${detailText})` - } - return displayName -} - -export function getDetailText (location) { - let detailText - if (location.type === 'home' || location.type === 'work') { - detailText = location.name - } - if (location.type === 'stop') { - detailText = location.id - } else if (location.type === 'recent' && location.timestamp) { - detailText = moment(location.timestamp).fromNow() - } - return detailText -} - -export function matchLatLon (location1, location2) { - if (!location1 || !location2) return location1 === location2 - return location1.lat === location2.lat && location1.lon === location2.lon -} - -export function itineraryToTransitive (itin, includeGeometry) { - // console.log('itineraryToTransitive', itin); - const tdata = { - journeys: [], - streetEdges: [], - places: [], - patterns: [], - routes: [], - stops: [] - } - const routes = {} - const stops = {} - let streetEdgeId = 0 - let patternId = 0 - - const journey = { - journey_id: 'itin', - journey_name: 'Iterarary-derived Journey', - segments: [] - } - - // add 'from' and 'to' places to the tdata places array - tdata.places.push({ - place_id: 'from', - place_lat: itin.legs[0].from.lat, - place_lon: itin.legs[0].from.lon - }) - tdata.places.push({ - place_id: 'to', - place_lat: itin.legs[itin.legs.length - 1].to.lat, - place_lon: itin.legs[itin.legs.length - 1].to.lon - }) - - itin.legs.forEach(leg => { - if ( - leg.mode === 'WALK' || - leg.mode === 'BICYCLE' || - leg.mode === 'CAR' || - leg.mode === 'MICROMOBILITY' - ) { - const fromPlaceId = leg.from.bikeShareId - ? `bicycle_rent_station_${leg.from.bikeShareId}` - : `itin_street_${streetEdgeId}_from` - const toPlaceId = leg.to.bikeShareId - ? `bicycle_rent_station_${leg.to.bikeShareId}` - : `itin_street_${streetEdgeId}_to` - - const segment = { - type: leg.mode, - streetEdges: [streetEdgeId], - from: { type: 'PLACE', place_id: fromPlaceId }, - to: { type: 'PLACE', place_id: toPlaceId } - } - // For TNC segments, draw using an arc - if (leg.mode === 'CAR' && leg.hailedCar) segment.arc = true - journey.segments.push(segment) - - tdata.streetEdges.push({ - edge_id: streetEdgeId, - geometry: leg.legGeometry - }) - tdata.places.push({ - place_id: fromPlaceId, - // Do not label the from place in addition to the to place. Otherwise, - // in some cases (bike rental station) the label for a single place will - // appear twice on the rendered transitive view. - // See https://github.com/conveyal/trimet-mod-otp/issues/152 - // place_name: leg.from.name, - place_lat: leg.from.lat, - place_lon: leg.from.lon - }) - tdata.places.push({ - place_id: toPlaceId, - place_name: leg.to.name, - place_lat: leg.to.lat, - place_lon: leg.to.lon - }) - streetEdgeId++ - } - if (isTransit(leg.mode)) { - // determine if we have valid inter-stop geometry - const hasInterStopGeometry = - leg.interStopGeometry && - leg.interStopGeometry.length === leg.intermediateStops.length + 1 - - // create leg-specific pattern - const ptnId = 'ptn_' + patternId - const pattern = { - pattern_id: ptnId, - pattern_name: 'Pattern ' + patternId, - route_id: leg.routeId, - stops: [] - } - - // add 'from' stop to stops dictionary and pattern object - stops[leg.from.stopId] = { - stop_id: leg.from.stopId, - stop_name: leg.from.name, - stop_lat: leg.from.lat, - stop_lon: leg.from.lon - } - pattern.stops.push({ stop_id: leg.from.stopId }) - - // add intermediate stops to stops dictionary and pattern object - for (const [i, stop] of leg.intermediateStops.entries()) { - stops[stop.stopId] = { - stop_id: stop.stopId, - stop_name: stop.name, - stop_lat: stop.lat, - stop_lon: stop.lon - } - pattern.stops.push({ - stop_id: stop.stopId, - geometry: hasInterStopGeometry && leg.interStopGeometry[i].points - }) - } - - // add 'to' stop to stops dictionary and pattern object - stops[leg.to.stopId] = { - stop_id: leg.to.stopId, - stop_name: leg.to.name, - stop_lat: leg.to.lat, - stop_lon: leg.to.lon - } - pattern.stops.push({ - stop_id: leg.to.stopId, - geometry: hasInterStopGeometry && leg.interStopGeometry[leg.interStopGeometry.length - 1].points - }) - - // add route to the route dictionary - routes[leg.routeId] = { - agency_id: leg.agencyId, - route_id: leg.routeId, - route_short_name: leg.routeShortName || '', - route_long_name: leg.routeLongName || '', - route_type: leg.routeType, - route_color: leg.routeColor - } - - // add the pattern to the tdata patterns array - tdata.patterns.push(pattern) - - // add the pattern refrerence to the journey object - journey.segments.push({ - type: 'TRANSIT', - patterns: [{ - pattern_id: ptnId, - from_stop_index: 0, - to_stop_index: (leg.intermediateStops.length + 2) - 1 - }] - }) - - patternId++ - } - }) - - // add the routes and stops to the tdata arrays - for (const k in routes) tdata.routes.push(routes[k]) - for (const k in stops) tdata.stops.push(stops[k]) - - // add the journey to the tdata journeys array - tdata.journeys.push(journey) - - // console.log('derived tdata', tdata); - return tdata -} - -export function isBikeshareStation (place) { - return place.place_id.lastIndexOf('bicycle_rent_station') !== -1 -} diff --git a/lib/util/profile.js b/lib/util/profile.js deleted file mode 100644 index ed79ed806..000000000 --- a/lib/util/profile.js +++ /dev/null @@ -1,180 +0,0 @@ -export function filterProfileOptions (response) { - // Filter out similar options. TODO: handle on server? - const optStrs = [] - const filteredIndices = [] - - const filteredProfile = response.otp.profile.filter((option, i) => { - let optStr = option.access.map(a => a.mode).join('/') - if (option.transit) { - optStr += ' to ' + option.transit.map(transit => { - return transit.routes.map(route => route.id).join('/') - }).join(',') - } - if (optStrs.indexOf(optStr) !== -1) return false - optStrs.push(optStr) - filteredIndices.push(i) - return true - }) - - const filteredJourneys = response.otp.journeys.filter((journey, i) => filteredIndices.indexOf(i) !== -1) - - response.otp.profile = filteredProfile - response.otp.journeys = filteredJourneys - return response -} - -/** profileOptionsToItineraries **/ - -export function profileOptionsToItineraries (options, query) { - return options.map(option => optionToItinerary(option, query)) -} - -// helper functions for profileOptionsToItineraries: - -function optionToItinerary (option, query) { - const itin = { - duration: option.time, - legs: [], - walkTime: 0, - waitingTime: 0 - } - - // access leg - if (option.access && option.access.length > 0) { - if (option.access[0].mode === 'BICYCLE_RENT') { - let status = 'WALK_ON' - const walkOnEdges = [] - const bikeEdges = [] - const walkOffEdges = [] - let onStationName - let walkOnTime = 0 - let offStationName - let walkOffTime = 0 - option.access[0].streetEdges.forEach(edge => { - // check if we're returning the bike - if (edge.bikeRentalOffStation) { - status = 'WALK_OFF' - offStationName = edge.bikeRentalOffStation.name - } - - if (status === 'WALK_ON') { - walkOnEdges.push(edge) - walkOnTime += edge.distance - } else if (status === 'BIKE') { - bikeEdges.push(edge) - } else if (status === 'WALK_OFF') { - walkOffEdges.push(edge) - walkOffTime += edge.distance - } - - // check if we're picking up the bike - if (edge.bikeRentalOnStation) { - status = 'BIKE' - onStationName = edge.bikeRentalOnStation.name - } - }) - - itin.walkTime += (walkOnTime + walkOffTime) - - // create the 'on' walk leg - itin.legs.push({ - mode: 'WALK', - duration: walkOnTime, - transitLeg: false, - from: { - name: locationString(query && query.from.name, 'Destination') - }, - to: { - name: onStationName - } - }) - - // create the bike leg - itin.legs.push({ - mode: 'BICYCLE_RENT', - duration: option.time - walkOnTime - walkOffTime, - transitLeg: false, - from: { - name: onStationName - }, - to: { - name: offStationName - } - }) - - // create the 'off' walk leg - itin.legs.push({ - mode: 'WALK', - duration: walkOffTime, - transitLeg: false, - from: { - name: offStationName - }, - to: { - name: locationString(query && query.to.name, 'Destination') - } - }) - } else { - itin.legs.push(accessToLeg(option.access[0], query && query.from.name, option.transit ? null : query && query.to.name)) - if (option.access[0].mode === 'WALK') itin.walkTime += option.access[0].time - } - } - - // transit legs - if (option.transit) { - option.transit.forEach(transit => { - itin.legs.push({ - transitLeg: true, - mode: transit.mode, - from: { - name: transit.fromName - }, - to: { - name: transit.toName - }, - routes: transit.routes, - duration: transit.rideStats.avg, - averageWait: transit.waitStats.avg - }) - itin.waitingTime += transit.waitStats.avg - }) - } - - // egress leg - if (option.egress && option.egress.length > 0) { - // find the origin name, for transit trips - const origin = option.transit ? option.transit[option.transit.length - 1].toName : null - - itin.legs.push(accessToLeg(option.egress[0], origin, query && query.to.name)) - if (option.egress[0].mode === 'WALK') itin.walkTime += option.egress[0].time - } - - // construct summary - if (option.transit) { - itin.summary = 'Transit' - } else { - if (option.modes.length === 1 && option.modes[0] === 'bicycle') itin.summary = 'Bicycle' - else if (option.modes.length === 1 && option.modes[0] === 'walk') itin.summary = 'Walk' - else if (option.modes.indexOf('bicycle_rent') !== -1) itin.summary = 'Bikeshare' - } - - return itin -} - -function accessToLeg (access, origin, destination) { - return { - mode: access.mode, - duration: access.time, - transitLeg: false, - from: { - name: locationString(origin, 'Origin') - }, - to: { - name: locationString(destination, 'Destination') - } - } -} - -function locationString (str, defaultStr) { - return str ? str.split(',')[0] : defaultStr -} diff --git a/lib/util/query-params.js b/lib/util/query-params.js deleted file mode 100644 index 94ebf17e2..000000000 --- a/lib/util/query-params.js +++ /dev/null @@ -1,546 +0,0 @@ -import { - isTransit, - isAccessMode, - isCar, - hasTransit, - hasBike, - hasMicromobility -} from './itinerary' -import { getItem } from './storage' -import { getCurrentDate, getCurrentTime } from './time' - -/** - * name: the default name of the parameter used for internal reference and API calls - * - * routingTypes: array of routing type(s) (ITINERARY, PROFILE, or both) this param applies to - * - * applicable: an optional function (accepting the current full query as a - * parameter) indicating whether this query parameter is applicable to the query. - * (Applicability is assumed if this function is not provided.) - * - * default: the default value for this parameter. The default can be also be a - * function that gets executed when accessing the default value. When the value - * is a funciton, it will take an argument of the current config of the otp-rr - * store. This is needed when a brand new time-dependent value is desired to be - * calculated. It's also helpful for producing tests that have consistent data - * output. - * - * itineraryRewrite: an optional function for translating the key and/or value - * for ITINERARY mode only (e.g. 'to' is rewritten as 'toPlace'). Accepts the - * intial internal value as a function parameter. - * - * profileRewrite: an optional function for translating the value for PROFILE mode - * - * label: a text label for for onscreen display. May either be a text string or a - * function (accepting the current full query as a parameter) returning a string - * - * selector: the default type of UI selector to use in the form. Can be one of: - * - DROPDOWN: a standard drop-down menu selector - * - * options: an array of text/value pairs used with a dropdown selector - * - * TODO: validation system for rewrite functions and/or better user documentation - * TODO: alphabetize below list - */ - -// FIXME: Use for parsing URL values? -// const stringToLocation = string => { -// const split = string.split(',') -// return split.length === 2 -// ? {lat: split[0], lon: split[1]} -// : {lat: null, lon: null} -// } - -const formatPlace = (location, alternateName) => { - if (!location) return null - const name = location.name || `${alternateName || 'Place'} (${location.lat},${location.lon})` - return `${name}::${location.lat},${location.lon}` -} - -// Load stored default query settings from local storage -let storedSettings = getItem('defaultQuery', {}) - -const queryParams = [ - { /* from - the trip origin. stored internally as a location (lat/lon/name) object */ - name: 'from', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: null, - itineraryRewrite: value => ({ fromPlace: formatPlace(value, 'Origin') }), - profileRewrite: value => ({ from: { lat: value.lat, lon: value.lon } }) - // FIXME: Use for parsing URL values? - // fromURL: stringToLocation - }, - - { /* to - the trip destination. stored internally as a location (lat/lon/name) object */ - name: 'to', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: null, - itineraryRewrite: value => ({ toPlace: formatPlace(value, 'Destination') }), - profileRewrite: value => ({ to: { lat: value.lat, lon: value.lon } }) - // FIXME: Use for parsing URL values? - // fromURL: stringToLocation - }, - - { /* date - the date of travel, in MM-DD-YYYY format */ - name: 'date', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: getCurrentDate - }, - - { /* time - the arrival/departure time for an itinerary trip, in HH:mm format */ - name: 'time', - routingTypes: [ 'ITINERARY' ], - default: getCurrentTime - }, - - { /* departArrive - whether this is a depart-at, arrive-by, or leave-now trip */ - name: 'departArrive', - routingTypes: [ 'ITINERARY' ], - default: 'NOW', - itineraryRewrite: value => ({ arriveBy: (value === 'ARRIVE') }) - }, - - { /* startTime - the start time for a profile trip, in HH:mm format */ - name: 'startTime', - routingTypes: [ 'PROFILE' ], - default: '07:00' - }, - - { /* endTime - the end time for a profile trip, in HH:mm format */ - name: 'endTime', - routingTypes: [ 'PROFILE' ], - default: '09:00' - }, - - { /* mode - the allowed modes for a trip, as a comma-separated list */ - name: 'mode', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 'WALK,TRANSIT', // TODO: make this dependent on routingType? - profileRewrite: value => { - const accessModes = [] - const directModes = [] - const transitModes = [] - - if (value && value.length > 0) { - value.split(',').forEach(m => { - if (isTransit(m)) transitModes.push(m) - if (isAccessMode(m)) { - accessModes.push(m) - // TODO: make configurable whether direct-driving is considered - if (!isCar(m)) directModes.push(m) - } - }) - } - - return { accessModes, directModes, transitModes } - } - }, - - { /* showIntermediateStops - whether response should include intermediate stops for transit legs */ - name: 'showIntermediateStops', - routingTypes: [ 'ITINERARY' ], - default: true - }, - - { /* maxWalkDistance - the maximum distance in meters the user will walk to transit. */ - name: 'maxWalkDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('WALK') !== -1, - default: 1207, // 3/4 mi. - selector: 'DROPDOWN', - label: 'Maximum Walk', - options: [ - { - text: '1/10 mile', - value: 160.9 - }, { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '5 miles', - value: 8047 - } - ] - }, - - { /* maxBikeDistance - the maximum distance in meters the user will bike. Not - * actually an OTP parameter (maxWalkDistance doubles for biking) but we - * store it separately internally in order to allow different default values, - * options, etc. Translated to 'maxWalkDistance' via the rewrite function. - */ - name: 'maxBikeDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('BICYCLE') !== -1, - default: 4828, // 3 mi. - selector: 'DROPDOWN', - label: 'Maximum Bike', - options: [ - { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '3 miles', - value: 4828 - }, { - text: '5 miles', - value: 8047 - }, { - text: '10 miles', - value: 16093 - }, { - text: '20 miles', - value: 32187 - }, { - text: '30 miles', - value: 48280 - } - ], - itineraryRewrite: value => ({ - maxWalkDistance: value, - // ensures that the value is repopulated when loaded from URL params - maxBikeDistance: value - }) - }, - - { /* optimize -- how to optimize a trip (non-bike, non-micromobility trips) */ - name: 'optimize', - applicable: query => hasTransit(query.mode) && !hasBike(query.mode), - routingTypes: [ 'ITINERARY' ], - default: 'QUICK', - selector: 'DROPDOWN', - label: 'Optimize for', - options: [ - { - text: 'Speed', - value: 'QUICK' - }, { - text: 'Fewest Transfers', - value: 'TRANSFERS' - } - ] - }, - - { /* optimizeBike -- how to optimize an bike-based trip */ - name: 'optimizeBike', - applicable: query => hasBike(query.mode), - routingTypes: [ 'ITINERARY' ], - default: 'SAFE', - selector: 'DROPDOWN', - label: 'Optimize for', - options: query => { - const opts = [{ - text: 'Speed', - value: 'QUICK' - }, { - text: 'Bike-Friendly Trip', - value: 'SAFE' - }, { - text: 'Flat Trip', - value: 'FLAT' - }] - - // Include transit-specific option, if applicable - if (hasTransit(query.mode)) { - opts.splice(1, 0, { - text: 'Fewest Transfers', - value: 'TRANSFERS' - }) - } - - return opts - }, - itineraryRewrite: value => ({ optimize: value }) - }, - - { /* maxWalkTime -- the maximum time the user will spend walking in minutes */ - name: 'maxWalkTime', - routingTypes: [ 'PROFILE' ], - default: 15, - selector: 'DROPDOWN', - label: 'Max Walk Time', - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('WALK') !== -1, - options: [ - { - text: '5 minutes', - value: 5 - }, { - text: '10 minutes', - value: 10 - }, { - text: '15 minutes', - value: 15 - }, { - text: '20 minutes', - value: 20 - }, { - text: '30 minutes', - value: 30 - }, { - text: '45 minutes', - value: 45 - }, { - text: '1 hour', - value: 60 - } - ] - }, - - { /* walkSpeed -- the user's walking speed in m/s */ - name: 'walkSpeed', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 1.34, - selector: 'DROPDOWN', - label: 'Walk Speed', - applicable: query => query.mode && query.mode.indexOf('WALK') !== -1, - options: [ - { - text: '2 MPH', - value: 0.89 - }, { - text: '3 MPH', - value: 1.34 - }, { - text: '4 MPH', - value: 1.79 - } - ] - }, - - { /* maxBikeTime -- the maximum time the user will spend biking in minutes */ - name: 'maxBikeTime', - routingTypes: [ 'PROFILE' ], - default: 20, - selector: 'DROPDOWN', - label: 'Max Bike Time', - applicable: query => query.mode && hasTransit(query.mode) && query.mode.indexOf('BICYCLE') !== -1, - options: [ - { - text: '5 minutes', - value: 5 - }, { - text: '10 minutes', - value: 10 - }, { - text: '15 minutes', - value: 15 - }, { - text: '20 minutes', - value: 20 - }, { - text: '30 minutes', - value: 30 - }, { - text: '45 minutes', - value: 45 - }, { - text: '1 hour', - value: 60 - } - ] - }, - - { /* bikeSpeed -- the user's bikeSpeed speed in m/s */ - name: 'bikeSpeed', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 3.58, - selector: 'DROPDOWN', - label: 'Bicycle Speed', - applicable: query => query.mode && query.mode.indexOf('BICYCLE') !== -1, - options: [ - { - text: '6 MPH', - value: 2.68 - }, { - text: '8 MPH', - value: 3.58 - }, { - text: '10 MPH', - value: 4.47 - }, { - text: '12 MPH', - value: 5.36 - } - ] - }, - - { /* maxEScooterDistance - the maximum distance in meters the user will ride - * an E-scooter. Not actually an OTP parameter (maxWalkDistance doubles for - * any non-transit mode except for car) but we store it separately - * internally in order to allow different default values, options, etc. - * Translated to 'maxWalkDistance' via the rewrite function. - */ - name: 'maxEScooterDistance', - routingTypes: [ 'ITINERARY' ], - applicable: query => query.mode && hasTransit(query.mode) && hasMicromobility(query.mode), - default: 4828, // 3 mi. - selector: 'DROPDOWN', - label: 'Maximum E-scooter Distance', - options: [ - { - text: '1/4 mile', - value: 402.3 - }, { - text: '1/2 mile', - value: 804.7 - }, { - text: '3/4 mile', - value: 1207 - }, { - text: '1 mile', - value: 1609 - }, { - text: '2 miles', - value: 3219 - }, { - text: '3 miles', - value: 4828 - }, { - text: '5 miles', - value: 8047 - }, { - text: '10 miles', - value: 16093 - }, { - text: '20 miles', - value: 32187 - }, { - text: '30 miles', - value: 48280 - } - ], - itineraryRewrite: value => ({ - maxWalkDistance: value, - // ensures that the value is repopulated when loaded from URL params - maxEScooterDistance: value - }) - }, - - { /* bikeSpeed -- the user's bikeSpeed speed in m/s */ - name: 'watts', - routingTypes: [ 'ITINERARY', 'PROFILE' ], - default: 250, - selector: 'DROPDOWN', - label: 'E-scooter Power', - // this configuration should only be allowed for personal E-scooters as these - // settings will be defined by the vehicle type of an E-scooter being rented - applicable: query => ( - query.mode && - query.mode.indexOf('MICROMOBILITY') !== -1 && - query.mode.indexOf('MICROMOBILITY_RENT') === -1 - ), - options: [ - { - text: 'Kid\'s hoverboard (6mph)', - value: 125 - }, { - text: 'Entry-level scooter (11mph)', - value: 250 - }, { - text: 'Robust E-scooter (18mph)', - value: 500 - }, { - text: 'Powerful E-scooter (24mph)', - value: 1500 - } - ], - // rewrite a few other values to add some baseline assumptions about the - // vehicle - itineraryRewrite: value => { - const watts = value - // the maximum cruising and downhill speed. Units in m/s - let maximumMicromobilitySpeed - let weight - // see https://en.wikipedia.org/wiki/Human_body_weight#Average_weight_around_the_world - // estimate is for an average North American human with clothes and stuff - // units are in kg - const TYPICAL_RIDER_WEIGHT = 90 - switch (watts) { - case 125: - // exemplar: Swagtron Turbo 5 hoverboard (https://swagtron.com/product/recertified-swagtron-turbo-five-hoverboard-classic/) - maximumMicromobilitySpeed = 2.8 // ~= 6mph - weight = TYPICAL_RIDER_WEIGHT + 9 - break - case 250: - // exemplar: Xiaomi M365 (https://www.gearbest.com/skateboard/pp_596618.html) - maximumMicromobilitySpeed = 5 // ~= 11.5mph - weight = TYPICAL_RIDER_WEIGHT + 12.5 - break - case 500: - // exemplar: Razor EcoSmart Metro (https://www.amazon.com/Razor-EcoSmart-Metro-Electric-Scooter/dp/B002ZDAEIS?SubscriptionId=AKIAJMXJ2YFJTEDLQMUQ&tag=digitren08-20&linkCode=xm2&camp=2025&creative=165953&creativeASIN=B002ZDAEIS&ascsubtag=15599460143449ocb) - maximumMicromobilitySpeed = 8 // ~= 18mph - weight = TYPICAL_RIDER_WEIGHT + 30 - break - case 1000: - // exemplar: Boosted Rev (https://boostedboards.com/vehicles/scooters/boosted-rev) - maximumMicromobilitySpeed = 11 // ~= 24mph - weight = TYPICAL_RIDER_WEIGHT + 21 - break - } - return {maximumMicromobilitySpeed, watts, weight} - } - }, - - { /* ignoreRealtimeUpdates -- if true, do not use realtime updates in routing */ - name: 'ignoreRealtimeUpdates', - routingTypes: [ 'ITINERARY' ], - default: false - }, - - { /* companies -- tnc companies to query */ - name: 'companies', - routingTypes: [ 'ITINERARY' ], - default: null - }, - - { /* wheelchair -- whether the user requires a wheelchair-accessible trip */ - name: 'wheelchair', - routingTypes: [ 'ITINERARY' ], - default: false, - selector: 'CHECKBOX', - label: 'Wheelchair Accessible', - applicable: (query, config) => { - if (!query.mode || !config.modes) return false - const configModes = (config.modes.accessModes || []).concat(config.modes.transitModes || []) - for (const mode of query.mode.split(',')) { - const configMode = configModes.find(m => m.mode === mode) - if (!configMode || !configMode.showWheelchairSetting) continue - if (configMode.company && (!query.companies || !query.companies.split(',').includes(configMode.company))) continue - return true - } - } - } -] -// Iterate over stored settings and update query param defaults. -// FIXME: this does not get updated if the user defaults are cleared -queryParams.forEach(param => { - if (param.name in storedSettings) { - param.default = storedSettings[param.name] - param.userDefaultOverride = true - } -}) - -export default queryParams diff --git a/lib/util/query.js b/lib/util/query.js deleted file mode 100644 index 5779108c7..000000000 --- a/lib/util/query.js +++ /dev/null @@ -1,206 +0,0 @@ -import qs from 'qs' - -import { getTransitModes, hasTransit, isAccessMode, toSentenceCase } from './itinerary' -import { coordsToString, matchLatLon, stringToCoords } from './map' -import queryParams from './query-params' -import { getActiveSearch } from './state' -import { getCurrentTime, getCurrentDate } from './time' - -/* The list of default parameters considered in the settings panel */ - -export const defaultParams = [ - 'wheelchair', - 'maxWalkDistance', - 'maxWalkTime', - 'walkSpeed', - 'maxBikeDistance', - 'maxBikeTime', - 'bikeSpeed', - 'optimize', - 'optimizeBike', - 'maxEScooterDistance', - 'watts' -] - -/* A function to retrieve a property value from an entry in the query-params - * table, checking for either a static value or a function */ - -export function getQueryParamProperty (paramInfo, property, query) { - return typeof paramInfo[property] === 'function' - ? paramInfo[property](query) - : paramInfo[property] -} - -export function ensureSingleAccessMode (queryModes) { - // Count the number of access modes - const accessCount = queryModes.filter(m => isAccessMode(m)).length - - // If multiple access modes are specified, keep only the first one - if (accessCount > 1) { - const firstAccess = queryModes.find(m => isAccessMode(m)) - queryModes = queryModes.filter(m => !isAccessMode(m) || m === firstAccess) - - // If no access modes are specified, add 'WALK' as the default - } else if (accessCount === 0) { - queryModes.push('WALK') - } - - return queryModes -} - -export function getUrlParams () { - return qs.parse(window.location.href.split('?')[1]) -} - -export function getOtpUrlParams () { - return Object.keys(getUrlParams()).filter(key => !key.startsWith('ui_')) -} - -function findLocationType (location, locations = [], types = ['home', 'work', 'suggested']) { - const match = locations.find(l => matchLatLon(l, location)) - return match && types.indexOf(match.type) !== -1 ? match.type : null -} - -export function summarizeQuery (query, locations = []) { - const from = findLocationType(query.from, locations) || query.from.name.split(',')[0] - const to = findLocationType(query.to, locations) || query.to.name.split(',')[0] - const mode = hasTransit(query.mode) - ? 'Transit' - : toSentenceCase(query.mode) - return `${mode} from ${from} to ${to}` -} - -/** - * Assemble any UI-state properties to be tracked via URL into a single object - * TODO: Expand to include additional UI properties - */ - -export function getUiUrlParams (otpState) { - const activeSearch = getActiveSearch(otpState) - const uiParams = { - ui_activeItinerary: activeSearch ? activeSearch.activeItinerary : 0, - ui_activeSearch: otpState.activeSearchId - } - return uiParams -} - -export function getTripOptionsFromQuery (query, keepPlace = false) { - const options = Object.assign({}, query) - // Delete time/date options and from/to - delete options.time - delete options.departArrive - delete options.date - if (!keepPlace) { - delete options.from - delete options.to - } - return options -} - -/** - * Gets the default query param by executing the default value function with the - * provided otp config if the default value is a function. - */ -function getDefaultQueryParamValue (param, config) { - return typeof param.default === 'function' ? param.default(config) : param.default -} - -/** - * Determines whether the specified query differs from the default query, i.e., - * whether the user has modified any trip options (including mode) from their - * default values. - */ -export function isNotDefaultQuery (query, config) { - const activeModes = query.mode.split(',') - const defaultModes = getTransitModes(config).concat(['WALK']) - let queryIsDifferent = false - const modesEqual = (activeModes.length === defaultModes.length) && - activeModes.sort().every((value, index) => { return value === defaultModes.sort()[index] }) - - if (!modesEqual) { - queryIsDifferent = true - } else { - defaultParams.forEach(param => { - const paramInfo = queryParams.find(qp => qp.name === param) - // Check that the parameter applies to the specified routingType - if (!paramInfo.routingTypes.includes(query.routingType)) return - // Check that the applicability test (if provided) is satisfied - if (typeof paramInfo.applicable === 'function' && - !paramInfo.applicable(query, config)) return - if (query[param] !== getDefaultQueryParamValue(paramInfo, config)) { - queryIsDifferent = true - } - }) - } - return queryIsDifferent -} - -/** - * Get the default query to OTP based on the given config. - * - * @param config the config in the otp-rr store. - */ -export function getDefaultQuery (config) { - const defaultQuery = { routingType: 'ITINERARY' } - queryParams.filter(qp => 'default' in qp).forEach(qp => { - defaultQuery[qp.name] = getDefaultQueryParamValue(qp, config) - }) - return defaultQuery -} - -/** - * Create a otp query based on a the url params. - * - * @param {Object} params An object representing the parsed querystring of url - * params. - * @param config the config in the otp-rr store. - */ -export function planParamsToQuery (params, config) { - const query = {} - for (var key in params) { - switch (key) { - case 'fromPlace': - query.from = parseLocationString(params.fromPlace) - break - case 'toPlace': - query.to = parseLocationString(params.toPlace) - break - case 'arriveBy': - query.departArrive = params.arriveBy === 'true' - ? 'ARRIVE' - : params.arriveBy === 'false' - ? 'DEPART' - : 'NOW' - break - case 'date': - query.date = params.date || getCurrentDate(config) - break - case 'time': - query.time = params.time || getCurrentTime(config) - break - default: - if (!isNaN(params[key])) query[key] = parseFloat(params[key]) - else query[key] = params[key] - } - } - return query -} - -/** - * OTP allows passing a location in the form '123 Main St::lat,lon', so we check - * for the double colon and parse the coordinates accordingly. - */ -function parseLocationString (value) { - const parts = value.split('::') - const coordinates = parts[1] - ? stringToCoords(parts[1]) - : stringToCoords(parts[0]) - const name = parts[1] - ? parts[0] - : coordsToString(coordinates) - return coordinates.length === 2 ? { - name: name || null, - lat: coordinates[0] || null, - lon: coordinates[1] || null - } : null -} diff --git a/lib/util/reverse.js b/lib/util/reverse.js deleted file mode 100644 index 8f8f93409..000000000 --- a/lib/util/reverse.js +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: add reverse geocode for map click -// export async function reversePelias (point) { -// const location = {lon: point.lng, lat: point.lat} -// const apiKey = getConfigProperty('MAPZEN_TURN_BY_TURN_KEY') -// const params = { -// api_key: apiKey, -// ...location -// } -// const url = `https://search.mapzen.com/v1/reverse?${qs.stringify(params)}` -// const response = await fetch(url) -// return await response.json() -// } diff --git a/lib/util/state.js b/lib/util/state.js index 8019564e8..cc22fc24c 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -1,5 +1,8 @@ +import coreUtils from '@opentripplanner/core-utils' import isEqual from 'lodash.isequal' +import { MainPanelContent } from '../actions/ui' + /** * Get the active search object * @param {Object} otpState the OTP state object @@ -135,3 +138,44 @@ export function getShowUserSettings (otpState) { export function getStopViewerConfig (otpState) { return otpState.config.stopViewer } + +/** + * Assemble any UI-state properties to be tracked via URL into a single object + * TODO: Expand to include additional UI properties + */ +export function getUiUrlParams (otpState) { + const activeSearch = getActiveSearch(otpState) + const uiParams = { + ui_activeItinerary: activeSearch ? activeSearch.activeItinerary : 0, + ui_activeSearch: otpState.activeSearchId + } + return uiParams +} + +// Set default title to the original document title (on load) set in index.html +const DEFAULT_TITLE = document.title + +export function getTitle (state) { + // Override title can optionally be provided in config.yml + const { config, ui, user } = state.otp + let title = config.title || DEFAULT_TITLE + const { mainPanelContent, viewedRoute, viewedStop } = ui + switch (mainPanelContent) { + case MainPanelContent.ROUTE_VIEWER: + title += ' | Route' + if (viewedRoute && viewedRoute.routeId) title += ` ${viewedRoute.routeId}` + break + case MainPanelContent.STOP_VIEWER: + title += ' | Stop' + if (viewedStop && viewedStop.stopId) title += ` ${viewedStop.stopId}` + break + default: + const activeSearch = getActiveSearch(state.otp) + if (activeSearch) { + title += ` | ${coreUtils.query.summarizeQuery(activeSearch.query, user.locations)}` + } + break + } + // if (printView) title += ' | Print' + return title +} diff --git a/lib/util/storage.js b/lib/util/storage.js deleted file mode 100644 index 4d9b7d88c..000000000 --- a/lib/util/storage.js +++ /dev/null @@ -1,42 +0,0 @@ -// Prefix to use with local storage keys. -const STORAGE_PREFIX = 'otp' - -/** - * Store a javascript object at the specified key. - */ -export function storeItem (key, object) { - window.localStorage.setItem(`${STORAGE_PREFIX}.${key}`, JSON.stringify(object)) -} - -/** - * Retrieve a javascript object at the specified key. If not found, defaults to - * null or, the optionally provided notFoundValue. - */ -export function getItem (key, notFoundValue = null) { - let itemAsString - try { - itemAsString = window.localStorage.getItem(`${STORAGE_PREFIX}.${key}`) - const json = JSON.parse(itemAsString) - if (json) return json - else return notFoundValue - } catch (e) { - // Catch any errors associated with parsing bad JSON. - console.warn(e, itemAsString) - return notFoundValue - } -} - -/** - * Remove item at specified key. - */ -export function removeItem (key) { - window.localStorage.removeItem(`${STORAGE_PREFIX}.${key}`) -} - -/** - * Generate a random ID. This might not quite be a UUID, but it serves our - * purposes for now. - */ -export function randId () { - return Math.random().toString(36).substr(2, 9) -} diff --git a/lib/util/time.js b/lib/util/time.js deleted file mode 100644 index be7e5964c..000000000 --- a/lib/util/time.js +++ /dev/null @@ -1,89 +0,0 @@ -import moment from 'moment' -import 'moment-timezone' - -// special constants for making sure the following date format is always sent to -// OTP regardless of whatever the user has configured as the display format -export const OTP_API_DATE_FORMAT = 'YYYY-MM-DD' -export const OTP_API_TIME_FORMAT = 'HH:mm' - -/** - * @param {[type]} config the OTP config object found in store - * @return {string} the config-defined time formatter or HH:mm (24-hr time) - */ -export function getTimeFormat (config) { - return (config.dateTime && config.dateTime.timeFormat) - ? config.dateTime.timeFormat - : OTP_API_TIME_FORMAT -} - -export function getDateFormat (config) { - return (config.dateTime && config.dateTime.dateFormat) - ? config.dateTime.dateFormat - : OTP_API_DATE_FORMAT -} - -export function getLongDateFormat (config) { - return (config.dateTime && config.dateTime.longDateFormat) - ? config.dateTime.longDateFormat - : 'D MMMM YYYY' -} - -/** - * Formats an elapsed time duration for display in narrative - * TODO: internationalization - * @param {number} seconds duration in seconds - * @returns {string} formatted text representation - */ -export function formatDuration (seconds) { - const dur = moment.duration(seconds, 'seconds') - let text = '' - if (dur.hours() > 0) text += dur.hours() + ' hr, ' - text += dur.minutes() + ' min' - return text -} - -/** - * Formats a time value for display in narrative - * TODO: internationalization/timezone - * @param {number} ms epoch time value in milliseconds - * @returns {string} formatted text representation - */ -export function formatTime (ms, options) { - return moment(ms + (options && options.offset ? options.offset : 0)) - .format(options && options.format ? options.format : OTP_API_TIME_FORMAT) -} - -/** - * Formats a seconds after midnight value for display in narrative - * @param {number} seconds time since midnight in seconds - * @param {string} timeFormat A valid moment.js time format - * @return {string} formatted text representation - */ -export function formatSecondsAfterMidnight (seconds, timeFormat) { - return moment().startOf('day').seconds(seconds).format(timeFormat) -} - -/** - * Formats current time for use in OTP query - * The conversion to the user's timezone is needed for testing purposes. - */ -export function getCurrentTime () { - return moment().tz(getUserTimezone()).format(OTP_API_TIME_FORMAT) -} - -/** - * Formats current date for use in OTP query - * The conversion to the user's timezone is needed for testing purposes. - */ -export function getCurrentDate (config) { - return moment().tz(getUserTimezone()).format(OTP_API_DATE_FORMAT) -} - -/** - * Get the timezone name that is set for the user that is currently looking at - * this website. Use a bit of hackery to force a specific timezone if in a - * test environment. - */ -export function getUserTimezone () { - return process.env.NODE_ENV === 'test' ? process.env.TZ : moment.tz.guess() -} diff --git a/lib/util/ui.js b/lib/util/ui.js deleted file mode 100644 index c430cc04e..000000000 --- a/lib/util/ui.js +++ /dev/null @@ -1,89 +0,0 @@ -import bowser from 'bowser' - -import { MainPanelContent } from '../actions/ui' -import { summarizeQuery } from './query' -import { getActiveSearch } from './state' - -// Set default title to the original document title (on load) set in index.html -const DEFAULT_TITLE = document.title - -export function isMobile () { - // TODO: consider using 3rd-party library? - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) -} - -/** - * Returns true if the user is using a [redacted] browser - */ -export function isIE () { - return bowser.name === 'Internet Explorer' -} - -/** - * Enables scrolling for a specified selector, while disabling scrolling for all - * other targets. This is adapted from https://stackoverflow.com/a/41601290/915811 - * and intended to fix issues with iOS elastic scrolling, e.g., - * https://github.com/conveyal/trimet-mod-otp/issues/92. - */ -export function enableScrollForSelector (selector) { - const _overlay = document.querySelector(selector) - let _clientY = null // remember Y position on touch start - - _overlay.addEventListener('touchstart', function (event) { - if (event.targetTouches.length === 1) { - // detect single touch - _clientY = event.targetTouches[0].clientY - } - }, false) - - _overlay.addEventListener('touchmove', function (event) { - if (event.targetTouches.length === 1) { - // detect single touch - disableRubberBand(event) - } - }, false) - - function disableRubberBand (event) { - const clientY = event.targetTouches[0].clientY - _clientY - - if (_overlay.scrollTop === 0 && clientY > 0) { - // element is at the top of its scroll - event.preventDefault() - } - - if (isOverlayTotallyScrolled() && clientY < 0) { - // element is at the top of its scroll - event.preventDefault() - } - } - - function isOverlayTotallyScrolled () { - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions - return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight - } -} - -export function getTitle (state) { - // Override title can optionally be provided in config.yml - const { config, ui, user } = state.otp - let title = config.title || DEFAULT_TITLE - const { mainPanelContent, viewedRoute, viewedStop } = ui - switch (mainPanelContent) { - case MainPanelContent.ROUTE_VIEWER: - title += ' | Route' - if (viewedRoute && viewedRoute.routeId) title += ` ${viewedRoute.routeId}` - break - case MainPanelContent.STOP_VIEWER: - title += ' | Stop' - if (viewedStop && viewedStop.stopId) title += ` ${viewedStop.stopId}` - break - default: - const activeSearch = getActiveSearch(state.otp) - if (activeSearch) { - title += ` | ${summarizeQuery(activeSearch.query, user.locations)}` - } - break - } - // if (printView) title += ' | Print' - return title -} diff --git a/package.json b/package.json index d4095dc05..af235bd87 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,26 @@ }, "homepage": "https://github.com/opentripplanner/otp-react-redux#readme", "dependencies": { - "@conveyal/geocoder-arcgis-geojson": "^0.0.2", - "@conveyal/lonlat": "^1.1.0", - "@mapbox/polyline": "^0.2.0", + "@opentripplanner/base-map": "^0.0.22", + "@opentripplanner/core-utils": "^0.0.22", + "@opentripplanner/endpoints-overlay": "^0.0.22", + "@opentripplanner/from-to-location-picker": "^0.0.22", + "@opentripplanner/geocoder": "^0.0.22", + "@opentripplanner/humanize-distance": "^0.0.22", + "@opentripplanner/icons": "^0.0.22", + "@opentripplanner/itinerary-body": "^0.0.22", + "@opentripplanner/location-field": "^0.0.22", + "@opentripplanner/location-icon": "^0.0.22", + "@opentripplanner/park-and-ride-overlay": "^0.0.22", + "@opentripplanner/printable-itinerary": "^0.0.22", + "@opentripplanner/route-viewer-overlay": "^0.0.22", + "@opentripplanner/stop-viewer-overlay": "^0.0.22", + "@opentripplanner/stops-overlay": "^0.0.22", + "@opentripplanner/transitive-overlay": "^0.0.22", + "@opentripplanner/trip-details": "^0.0.22", + "@opentripplanner/trip-form": "^0.0.22", + "@opentripplanner/trip-viewer-overlay": "^0.0.22", + "@opentripplanner/vehicle-rental-overlay": "^0.0.22", "bootstrap": "^3.3.7", "bowser": "^1.9.3", "clone": "^2.1.0", @@ -38,15 +55,11 @@ "currency-formatter": "^1.4.2", "d3-selection": "^1.3.0", "d3-zoom": "^1.7.1", - "deep-equal": "^1.0.1", "font-awesome": "^4.7.0", "haversine": "^1.1.0", "history": "^4.7.2", "immutability-helper": "^2.1.1", "isomorphic-fetch": "^2.2.1", - "isomorphic-mapzen-search": "^1.5.1", - "leaflet": "^1.5.1", - "leaflet.polylinemeasure": "github:ppete2/Leaflet.PolylineMeasure", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "lodash.memoize": "^4.1.2", @@ -57,12 +70,10 @@ "prop-types": "^15.6.0", "qs": "^6.3.0", "react": "^16.9.0", - "react-addons-shallow-compare": "^15.4.2", "react-bootstrap": "^0.32.1", "react-dates": "^6.0.2", "react-dom": "^16.9.0", "react-fontawesome": "^1.5.0", - "react-leaflet": "^2.4.0", "react-redux": "^7.1.0", "react-resize-detector": "^2.1.0", "react-router": "^5.0.1", @@ -71,9 +82,7 @@ "redux-actions": "^1.2.1", "redux-logger": "^2.7.4", "redux-thunk": "^2.3.0", - "throttle-debounce": "^2.0.1", "transitive-js": "^0.13.2", - "turf-along": "^3.0.12", "velocity-react": "^1.3.3" }, "devDependencies": { @@ -82,17 +91,23 @@ "enzyme-adapter-react-16": "^1.4.0", "enzyme-to-json": "^3.4.0", "es6-math": "^1.0.0", + "leaflet": "^1.6.0", "lodash.clonedeep": "^4.5.0", "mastarm": "^5.1.3", "nock": "^9.0.9", "react": "^16.9.0", "react-dom": "^16.9.0", + "react-leaflet": "^2.6.1", "redux-mock-store": "^1.5.3", - "semantic-release": "^15.13.12" + "semantic-release": "^15.13.12", + "styled-components": "^5.0.1" }, "peerDependencies": { + "leaflet": "^1.6.0", "react": ">=15.0.0", - "react-dom": ">=15.0.0" + "react-dom": ">=15.0.0", + "react-leaflet": "^2.6.1", + "styled-components": "^5.0.1" }, "jest": { "globalSetup": "/__tests__/test-utils/global-setup.js", diff --git a/yarn.lock b/yarn.lock index 43f90da95..63979883a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.3.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" @@ -40,6 +47,16 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.6.tgz#57adf96d370c9a63c241cd719f9111468578537a" + integrity sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg== + dependencies: + "@babel/types" "^7.8.6" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -110,6 +127,15 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-function-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" + integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" + "@babel/helper-get-function-arity@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" @@ -117,6 +143,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== + dependencies: + "@babel/types" "^7.8.3" + "@babel/helper-hoist-variables@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" @@ -205,6 +238,13 @@ dependencies: "@babel/types" "^7.4.4" +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== + dependencies: + "@babel/types" "^7.8.3" + "@babel/helper-wrap-function@^7.1.0", "@babel/helper-wrap-function@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -233,6 +273,15 @@ esutils "^2.0.2" js-tokens "^4.0.0" +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + "@babel/parser@7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.3.tgz#2c92469bac2b7fbff810b67fca07bd138b48af77" @@ -243,6 +292,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" + integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -927,6 +981,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.7.6": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -936,6 +997,15 @@ "@babel/parser" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/template@^7.8.3": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" @@ -951,6 +1021,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.4.5": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" + integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.6" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.1.3", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" @@ -960,6 +1045,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.8.3", "@babel/types@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" + integrity sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -976,7 +1070,7 @@ "@conveyal/lonlat" "^1.3.0" geocoder-arcgis "^2.0.4" -"@conveyal/lonlat@^1.1.0", "@conveyal/lonlat@^1.1.2", "@conveyal/lonlat@^1.3.0": +"@conveyal/lonlat@^1.1.2", "@conveyal/lonlat@^1.3.0", "@conveyal/lonlat@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@conveyal/lonlat/-/lonlat-1.4.0.tgz#18a5c1349078a779e710d24af11bc02b24127ba0" integrity sha512-ag1FcRuwRGAZgeZ4e3sUq+gblf1Pgma2c9SaIVXluIrgsZ9Lrq7xhCbV0ErN8chyg/OCOoG8m/l3mgzbycQCnQ== @@ -986,6 +1080,28 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@emotion/is-prop-valid@^0.8.3", "@emotion/is-prop-valid@^0.8.6": + version "0.8.7" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.7.tgz#803449993f436f9a6c67752251ea3fc492a1044c" + integrity sha512-OPkKzUeiid0vEKjZqnGcy2mzxjIlCffin+L2C02pdz/bVlt5zZZE2VzO0D3XOPnH0NEeF21QNKSXiZphjr4xiQ== + dependencies: + "@emotion/memoize" "0.7.4" + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/stylis@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@^0.7.4": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@jest/console@^24.7.1": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -1132,10 +1248,12 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^12.0.9" -"@mapbox/polyline@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@mapbox/polyline/-/polyline-0.2.0.tgz#6e25980744aa22331f94b645a542c02d3fcfee97" - integrity sha1-biWYB0SqIjMflLZFpULALT/P7pc= +"@mapbox/polyline@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/polyline/-/polyline-1.1.0.tgz#7bdd1da3a25c1d059af3897e0ee581b7c7b80243" + integrity sha512-NwiMS+AA/ZLIgd69YYXtQ0jtIBu8nQuVaRr4NxBoRjEd9ddKDoe4q8a51Ukk7Vj+jwQtbupqmvoLni1QhnJ4Pg== + dependencies: + meow "^5.0.0" "@nodelib/fs.scandir@2.1.1": version "2.1.1" @@ -1208,6 +1326,198 @@ universal-user-agent "^3.0.0" url-template "^2.0.8" +"@opentripplanner/base-map@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/base-map/-/base-map-0.0.22.tgz#bef9007ab9966336cd77e8d218c1ea0e2f439980" + integrity sha512-nCajhWENtb/uP4YPe33+aotdkhO294qr+MlYYnjSolVSVYySwzqJhCEbPTDkQfcKs/kqc8l1d8rgKwWQpEEJ2A== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/core-utils@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-0.0.22.tgz#c6e3d392b0c99311767add868491a87923aba769" + integrity sha512-beqyFQhocpdESRaIpzVPAPYtBu33YCOYwi3an8mUfnSEGHJpKLMj9Zf7BHBM5ymLmp16JqmCXAaR/EpV6A2GIg== + dependencies: + "@mapbox/polyline" "^1.1.0" + "@turf/along" "^6.0.1" + bowser "^2.7.0" + lodash.isequal "^4.5.0" + moment "^2.24.0" + moment-timezone "^0.5.27" + prop-types "^15.7.2" + qs "^6.9.1" + +"@opentripplanner/endpoints-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/endpoints-overlay/-/endpoints-overlay-0.0.22.tgz#6ed26d9e63c30fd9fd2b8798c986c46257d63061" + integrity sha512-DVHakmt+gU199UQTMGXgZ3nNCPbsZL3rjkxpxSH98BJAcUjSw7A/boVnGcwW7Q3GoX581WHUjdonQwgHfXvlAg== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/location-icon" "^0.0.22" + prop-types "^15.7.2" + styled-icons "^9.1.0" + +"@opentripplanner/from-to-location-picker@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/from-to-location-picker/-/from-to-location-picker-0.0.22.tgz#51aedd10eadc8e188d19ae214d7340f459761848" + integrity sha512-kDkkuCxGvZLNczPsZjG3K7Zd0P2xzA01k/Hs6GISQTDAtjxuf2sAUu4zhVao9TShltOfDHAjivhxpNM3H075Ig== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/location-icon" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/geocoder@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/geocoder/-/geocoder-0.0.22.tgz#6ccbc9af85c21ca9592ddcd2178fcb6b9dc3fa6f" + integrity sha512-cF7B0IUTN0Xu+WZoamMtpAVpxK5iSROSuSwFQphvixOuefV0K0Nxca0xlyeaF+G9FqO8c88J0AQVxoMpOx86UQ== + dependencies: + "@conveyal/geocoder-arcgis-geojson" "^0.0.2" + "@conveyal/lonlat" "^1.4.0" + isomorphic-mapzen-search "^1.5.1" + lodash.memoize "^4.1.2" + +"@opentripplanner/humanize-distance@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/humanize-distance/-/humanize-distance-0.0.22.tgz#6e2d1b4ae938e45be4eae3ddb41729e55222d741" + integrity sha512-50FREAdjyAbsARPeii1TJHVPNoF0VqPuOZixYs+KYo/e5T9f6x+tX6ypEVIoh+cVxms5cWwM91HVg0XfA5AZ2Q== + +"@opentripplanner/icons@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/icons/-/icons-0.0.22.tgz#88da23303ca8539f2d22567327409c68561bce29" + integrity sha512-KCO6a8XvRy79A2XnQgBmtUPP5gQcyB0pMMNp2mg727O/ANtKiustPKrzgWSFuakogEfr7d0rSVev7q1HlUVrgA== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/itinerary-body@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/itinerary-body/-/itinerary-body-0.0.22.tgz#7dbc934fca17798cb8b3d38993d0e65ef2069151" + integrity sha512-z38UYZVHLNJV7Q6zcW4+sOUyy5YnFgA6YT4Tr+0u2nSEgB5ru1+c59O35dOPi9EEGnNJ0g6G+SLJnSmNeVW92g== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/humanize-distance" "^0.0.22" + "@opentripplanner/icons" "^0.0.22" + "@opentripplanner/location-icon" "^0.0.22" + currency-formatter "^1.5.5" + moment "^2.24.0" + prop-types "^15.7.2" + react-resize-detector "^4.2.1" + velocity-react "^1.4.3" + +"@opentripplanner/location-field@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/location-field/-/location-field-0.0.22.tgz#8bc4b6baa3326b9ebc87e45eea03d3c0072631fe" + integrity sha512-ONq4SRRYyKw2aEwp+AJR8gBwqHsk10Rxk3Cm+d2xLva4g45xUp+OxKfNNbqI2HWouKxb5zsZxfeiDnqqyuethQ== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/geocoder" "^0.0.22" + "@opentripplanner/humanize-distance" "^0.0.22" + "@opentripplanner/location-icon" "^0.0.22" + prop-types "^15.7.2" + styled-icons "^9.1.0" + throttle-debounce "^2.1.0" + +"@opentripplanner/location-icon@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/location-icon/-/location-icon-0.0.22.tgz#85c59ec0c67c5d1bb167ad55543bdf5e5403d2a6" + integrity sha512-kW/8Ap6gjzEpE6ocxxXEaSh/iZSsjAkNG5sXB4gEpYmU4+2pflAGCsqiEAxz9jKMdxgzAkvm0caQAH9LkWF/Ig== + dependencies: + styled-icons "^9.1.0" + +"@opentripplanner/park-and-ride-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/park-and-ride-overlay/-/park-and-ride-overlay-0.0.22.tgz#f5c62d7cb432b576275c02d5bb425a81bcee033a" + integrity sha512-s39qW5joPlmY9KDTy3iet917sHsh2tdJQHXTxaxBGAARCT/n5ifOvyQEULjYlu3LUoAhxuPHkF3FOpnR3Crv7g== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/from-to-location-picker" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/printable-itinerary@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/printable-itinerary/-/printable-itinerary-0.0.22.tgz#1dd898768ec7a24a33a5363ede88e2d40edd7a7f" + integrity sha512-TTM91C/dM4m/a9yoHyXNBjWKHxN8XITe3arF1Z1sGtuUtxvAkeN0vMvb3sKyoNi1jlsbkGi2H12aGNPBvLB/Tg== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/humanize-distance" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/route-viewer-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/route-viewer-overlay/-/route-viewer-overlay-0.0.22.tgz#bd8bbf72b4843126eb81a24e1adac4a0a684a3b8" + integrity sha512-LUkSnKWr848RuZ0ccR6bLbqHKxh6jpBlpNzFAaa/qb4VTMZEM2DwRNpCpbnRCmXmoGEjdM7dvtRS7Vbz9My8lA== + dependencies: + "@mapbox/polyline" "^1.1.0" + "@opentripplanner/core-utils" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/stop-viewer-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/stop-viewer-overlay/-/stop-viewer-overlay-0.0.22.tgz#4b63b9ce736e98a3261817c0a51336ea5ff4f00d" + integrity sha512-S7hn+JA+fNQsUCjtC+IqcWhnCzThEGRCNVK/Ww1TY9AQ6kys4PRn1R7A9kMCKD19yHcg/F9rsGTauPhVFH7XPA== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/stops-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/stops-overlay/-/stops-overlay-0.0.22.tgz#f24a0214cef1e7c152ef3963e2835137e04ee6c5" + integrity sha512-Cj1a1oSt6n+2MfSaVrwgMZC0hPFfdsy3Ieqx+iu5/fumilq4BynCWNRklVCSSsOE0HKMSBj2vBmEexhu0AMJrQ== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/from-to-location-picker" "^0.0.22" + +"@opentripplanner/transitive-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/transitive-overlay/-/transitive-overlay-0.0.22.tgz#1a83fe90d21c51ad9442efd214e3ff18648e9c68" + integrity sha512-jVZex3dD29h5vTN2FEzJdd8etNdeBd4NqQ9zQo1uh3+px89xTaby2s7tw2I77HMgi3uEeQJpq0ItAPfUHEhhpQ== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + lodash.isequal "^4.5.0" + transitive-js "^0.13.2" + +"@opentripplanner/trip-details@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-details/-/trip-details-0.0.22.tgz#7c701c0ebed54342081a4eb3e566fa5a6fb0cf23" + integrity sha512-0ZCXYXX9VVKM/0ZepymLdn8mBttCYb4u2/PMLa7YZ+fuiGhi6gZBpvZf81oYzy7+r5TCDQ7sO9RIpnHfO4X/8g== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/humanize-distance" "^0.0.22" + moment "^2.24.0" + prop-types "^15.7.2" + styled-icons "^9.1.0" + velocity-react "^1.4.3" + +"@opentripplanner/trip-form@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-form/-/trip-form-0.0.22.tgz#fdb8c53747ab30dadc08ba7c99cb8ce67b076b56" + integrity sha512-MT3DeoignJiK4NvWg1tDQYZgzEIiS5LYEfVozDFrC5DNO5/iXAQK08bQ/vuTfn9n/z8bkrk8E27SVyGLMOKL1Q== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/icons" "^0.0.22" + moment "^2.17.1" + +"@opentripplanner/trip-viewer-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-viewer-overlay/-/trip-viewer-overlay-0.0.22.tgz#82831f1c6e3ee447fe624184412d9235d4b6ed49" + integrity sha512-tI/ObFNMAPz+c1mJJVmHpJwxoykosqx8G/7s4ZJQN0uwLg0n3Bf+w7Zrv4mQA9gbUR4oUTrN1IVt76LIyCbwNA== + dependencies: + "@mapbox/polyline" "^1.1.0" + "@opentripplanner/core-utils" "^0.0.22" + prop-types "^15.7.2" + +"@opentripplanner/vehicle-rental-overlay@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-0.0.22.tgz#cc247e8fdc4f4fa772569a5634c2566b38443588" + integrity sha512-f5apetx4oepdiq3sNMLQokr67Tt1VfRmjulNLMh3J/pxCwFo+Vhkg4DtjNlBzIF9LuuMGnMCjqIAuNKSb5DKGA== + dependencies: + "@opentripplanner/core-utils" "^0.0.22" + "@opentripplanner/from-to-location-picker" "^0.0.22" + lodash.memoize "^4.1.2" + prop-types "^15.7.2" + styled-icons "^9.1.0" + "@semantic-release/commit-analyzer@^6.1.0": version "6.2.0" resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-6.2.0.tgz#5cd25ce67ba9ba5b46e47457505e63629e186695" @@ -1281,6 +1591,237 @@ lodash "^4.17.4" read-pkg-up "^6.0.0" +"@styled-icons/boxicons-logos@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-logos/-/boxicons-logos-9.4.1.tgz#774e7f73839e834445dcc0ae4c0c356221767664" + integrity sha512-DN6Za9paJeWkoIXI1newSSfDALIR8ecaAsgzP3ZD+NRJHy6idW+1ouC5p3DlRM29+Cp4yJ/z3bH9whBDbd7YXQ== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/boxicons-regular@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-regular/-/boxicons-regular-9.4.1.tgz#742a1d0b5798c54f189b9278810d8110df8f9754" + integrity sha512-910zIWCB0JkEgk9I7zIIEdPc8xibg3xrT4Wo7gIu4lGEr4u9OOQT3c0ajJD7F+JlhWcWBVumzjfjOT8A5UjPJw== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/boxicons-solid@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-solid/-/boxicons-solid-9.4.1.tgz#5219a0c900f2ccc5dc9056c4ee2d1c180d9dbdf1" + integrity sha512-7FvU6KSnxBprsidta3/0grTkimYWDUnSZCyLscT1OwjmfyFPpnLNmJASQDrEtORF2GPw5Y2gc/q6h2RI4OnYcA== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/crypto@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/crypto/-/crypto-9.4.1.tgz#9abe2a3165f78a1592ea1d910aa1e1210b990fef" + integrity sha512-qIjzvaw0kFykDN7PTGD8d3HvFVoX7kW7nwlvvzW/pGhVzpUHE7Mh2Syc04HpoknCgO0fCjAzXaxi9Igu3nS9NA== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/entypo-social@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/entypo-social/-/entypo-social-9.5.0.tgz#d5c69c928dbcb83c39d8359e50c148bd497bcaff" + integrity sha512-yEpaJtTUriZSZPT/iqxgNONgxDYr0MHXANvoUW2CB0Ey4zTZst+2x7EJ9KgL2CjdBgA0bs8hnx5etGHUi8BtuA== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/entypo@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/entypo/-/entypo-9.5.0.tgz#170c484e53820ec683d338353e44bae71d773611" + integrity sha512-gy/CXs45g1o5c61BnSnpd/cb84sXIARPByOa354FkN0aUHqepbF2qtBjZ3Udho6zWpm1nGOOUiawQb53PnG9vg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/evil@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/evil/-/evil-9.4.1.tgz#159d5c026e7d0689b453684b719ecb13e4ea0fe5" + integrity sha512-ejCBH+aXrjtBM+iMJomYlZ/xiJ+NGHVlIfQmkgD6roxtJzMzAmhwXgC5S3p0no2n1EXluUdJiufrI/QtP7iNbA== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/fa-brands@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/fa-brands/-/fa-brands-9.4.1.tgz#ad329324875bb7db00ff5fb68ce98203fbe03b7a" + integrity sha512-zqlS/Y3mQLdBgRhZo1op5+twKYYwfS7OjqL90N+etokIWPqv1b7nwPWzwKoRpqDfCt/gyqmgxXqWufFd0CAx7Q== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/fa-regular@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/fa-regular/-/fa-regular-9.4.1.tgz#556b1840e97db031b5c07a7175b57103e3fd988b" + integrity sha512-tpgLOUPECtWNwVs7G82IaDkb1T13yNyDNZBB85sEO02ldHAZe6tt0Yi5gGCBExtU/p+4/jLTcU+pcoh/mySlVg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/fa-solid@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/fa-solid/-/fa-solid-9.4.1.tgz#d30de755f8fee6e932032e97fa592cb1eaa21d8e" + integrity sha512-ZyPxbCFmuuy3XotCI0SgHyUn3gTvQXm+iVnh+8r0gR8Kd88iTTubTqxgtdS0lrefaB4ayl6vEg20Dv9e9PvYVg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/feather@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/feather/-/feather-9.4.1.tgz#5e8a4193e70d477e9ad249f921c6d637f9fc631c" + integrity sha512-ucMOPhvaCp6N4lxL+z/66Kech20jAdMfewER732jhjV8P3FknoAY2+0CXXE+tBvJoJ/SOJZDOsW58Gr7yAWXOQ== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/foundation@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/foundation/-/foundation-9.5.0.tgz#011ac022ffcb20868385b97bf4fb173cceaf634d" + integrity sha512-O4Y62bfZfZjhZDXYtJYd/RsID1vpmMtgLSP54b0KbvY70hjdlFQZ0ZVR2fAzA0YSK90VjbYBbQDCCyqim4v3qA== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/heroicons-outline@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/heroicons-outline/-/heroicons-outline-9.5.0.tgz#235046e3310e10165b9587a122a60e83e94e51de" + integrity sha512-MMY5NjVVimhTHhk+ADR4/a2KpjIjbmzW+SNPRCKMWgGyqXx1NjBrD72bnvrzApLdMnDvEsM/DYK6DVUW7UZhKw== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/heroicons-solid@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/heroicons-solid/-/heroicons-solid-9.5.0.tgz#2ed99d05a9a82c742eb7667c75b0d9c319fdee10" + integrity sha512-dYQYdxZ0BkGpl3nGQmkvONF1+Yq+E6I9S7snba1mirCDWQpIdgylGm+Z9CM/OvdIlhr5da00D4oZu+2CiGdCFQ== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/icomoon@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/icomoon/-/icomoon-9.4.1.tgz#b75118a6d10cb8f5e7e15ae7a323009dd2f98e7e" + integrity sha512-oEw84P55EahJFdhQPBnUbvQqB08LuJKq75AkwaCSqk6w1DIEDbjY72lb6mbAwZB/AwNYeRVJJhTHaF1MNY103A== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/material@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/material/-/material-9.4.1.tgz#26169db802a10e333a5608aec2611878dd45f6fe" + integrity sha512-7Ucsm7RxCrnznZb+WDptBrGE45ZVBODm6J0e+fy0hvO9sjWAcuoKffYTcQEhDS2pqC1Rcc/nSZHDbqwZrpGJVw== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/octicons@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/octicons/-/octicons-9.4.1.tgz#9670b6a3fc70eb80961fa0771464d8dbde18a6ea" + integrity sha512-MHiMMah2agDpmk8JcNLgeZab74TO3elM8CIYIQahrucyVbHfCY7mX1z8OOyXZwrf/hgrBKK4nUM4jf0wxk70+Q== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/open-iconic@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/open-iconic/-/open-iconic-9.5.0.tgz#03c70256244ee4a1ed422563f3ce3d0270a6beb8" + integrity sha512-mSDnCPDKYXhXY+oNfgNKMj2+ltl63k8QIC6bCDsX18bYXDqkeEwKthfNw0minabyP4Tq+yJe/B03RH+EdQWmDg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/remix-fill@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/remix-fill/-/remix-fill-9.4.1.tgz#afc7141d0c5c0606ef190b0cebe4793a4449b389" + integrity sha512-OlxQudeM/u+IAHacSVwwiuSYBDSsQNKA4gr8WSFHpy1CEciLc/hE/aNJP+SLcBETdg/C/kJIj8JNme5B8aYIeg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/remix-line@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/remix-line/-/remix-line-9.4.1.tgz#1d4fdaef53f3c366a746f054d70dcd138a4e567e" + integrity sha512-NAdR8LD7ugeUhmd6W3v+GA5/6OBexe15fjQTt1kFHYclS+2nrLQ7MA4kF5SkwsRQNGPy+3CMN5cQoMy5R8NAtQ== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/styled-icon@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/styled-icon/-/styled-icon-9.4.1.tgz#9dc236c85afd89edc2bc7265ec7858cbc35d4234" + integrity sha512-qF0E2QOcyR1e6rh1QkKGsFt9vuayhZBJEKRa8erbHpOYLRNiEmoF1dLEVVMUKjnj5jAzzk0fNd8jSz0su3kpKg== + dependencies: + "@emotion/is-prop-valid" "^0.8.6" + tslib "^1.9.3" + +"@styled-icons/typicons@^9.4.1": + version "9.4.1" + resolved "https://registry.yarnpkg.com/@styled-icons/typicons/-/typicons-9.4.1.tgz#b201080bc3985cab1f0ff0e8827350aea14cb309" + integrity sha512-gV3W836B7k7FuNjZJt2H7WYcV+wPf7L5RYJQUL99lPdvADeOUhG2hIVIKvmS4F2Tc3XbyYbRmSBPTHtu4Fcftg== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@styled-icons/zondicons@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@styled-icons/zondicons/-/zondicons-9.5.0.tgz#27f184764f586f31aa9256905ab3db1cd833bfa8" + integrity sha512-26PYTzSr85tf1+P/N1zaqbLuJLG1YP9rp3QN1TsCcM19gT0Y3WzVUmiNHdv15jGO9bH/jpV9b7TjuT+1gkMPCw== + dependencies: + "@styled-icons/styled-icon" "^9.4.1" + tslib "^1.9.3" + +"@turf/along@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/along/-/along-6.0.1.tgz#595cecdc48fc7fcfa83c940a8e3eb24d4c2e04d4" + integrity sha512-6PptAcrsFR3o0Flpktk8Vo68W2txEVTh14zjoTVu+H5docd2+pv5/upA77bg3YFBoJgAxmUFt1leDdjReJ44BQ== + dependencies: + "@turf/bearing" "6.x" + "@turf/destination" "6.x" + "@turf/distance" "6.x" + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/bearing@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/bearing/-/bearing-6.0.1.tgz#8da5d17092e571f170cde7bfb2e5b0d74923c92d" + integrity sha512-mXY1NozqV9EFfBTbUItujwfqfQF0G/Xe2fzvnZle90ekPEUfhi4Dgf5JswJTd96J9LiT8kcd6Jonp5khnx0wIg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/destination@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a" + integrity sha512-MroK4nRdp7as174miCAugp8Uvorhe6rZ7MJiC9Hb4+hZR7gNFJyVKmkdDDXIoCYs6MJQsx0buI+gsCpKwgww0Q== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/distance@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" + integrity sha512-q7t7rWIWfkg7MP1Vt4uLjSEhe5rPfCO2JjpKmk7JC+QZKEQkuvHEqy3ejW1iC7Kw5ZcZNR3qdMGGz+6HnVwqvg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/helpers@6.x": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" + integrity sha512-vJvrdOZy1ngC7r3MDA7zIGSoIgyrkWcGnNIEaqn/APmw+bVLF2gAW7HIsdTxd12s5wQMqEpqIQrmrbRRZ0xC7g== + +"@turf/invariant@6.x": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.1.2.tgz#6013ed6219f9ac2edada9b31e1dfa5918eb0a2f7" + integrity sha512-WU08Ph8j0J2jVGlQCKChXoCtI50BB3yEH21V++V0T4cR1T27HKCxkehV2sYMwTierfMBgjwSwDIsxnR4/2mWXg== + dependencies: + "@turf/helpers" "6.x" + "@types/babel__core@^7.1.0": version "7.1.2" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" @@ -2312,6 +2853,16 @@ babel-plugin-react-require@^3.1.1: resolved "https://registry.yarnpkg.com/babel-plugin-react-require/-/babel-plugin-react-require-3.1.1.tgz#5c3d2564fa16b1e45212ed52519db147b1596106" integrity sha512-XFz+B0dWx41fnGnugzCWn5rOgrDHb150N5gFhUfO3BgYDCT25o4sofRtd9uUfqUHoRu+t4/r5Cr2RMPIKuCt2g== +"babel-plugin-styled-components@>= 1": + version "1.10.7" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz#3494e77914e9989b33cc2d7b3b29527a949d635c" + integrity sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-module-imports" "^7.0.0" + babel-plugin-syntax-jsx "^6.18.0" + lodash "^4.17.11" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -2347,7 +2898,7 @@ babel-plugin-syntax-flow@^6.18.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" integrity sha1-TDqyCiryaqIM0lmVw5jE63AxDI0= -babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: +babel-plugin-syntax-jsx@^6.18.0, babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= @@ -2966,6 +3517,11 @@ bowser@^1.9.3: resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== +bowser@^2.7.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" + integrity sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA== + boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -3412,6 +3968,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelize@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + caniuse-api@^1.5.2: version "1.6.1" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" @@ -4321,6 +4882,11 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= + css-color-names@0.0.4, css-color-names@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -4408,6 +4974,15 @@ css-selector-tokenizer@^0.7.0: fastparse "^1.1.1" regexpu-core "^1.0.0" +css-to-react-native@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756" + integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + css-tree@1.0.0-alpha.29: version "1.0.0-alpha.29" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" @@ -4591,6 +5166,15 @@ currency-formatter@^1.4.2: locale-currency "0.0.2" object-assign "^4.1.1" +currency-formatter@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/currency-formatter/-/currency-formatter-1.5.5.tgz#907790bb0b7f129c4a64d2924e0d7fa36db0cf52" + integrity sha512-PEsZ9fK2AwPBYgzWTtqpSckam7hFDkT8ZKFAOrsooR0XbydZEKuFioUzcc3DoT2mCDkscjf1XdT6Qq53ababZQ== + dependencies: + accounting "^0.4.1" + locale-currency "0.0.2" + object-assign "^4.1.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -4796,7 +5380,7 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" -deep-equal@^1.0.0, deep-equal@^1.0.1: +deep-equal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= @@ -5947,6 +6531,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-glob@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602" @@ -6224,9 +6813,9 @@ forever-agent@~0.6.1: integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37" - integrity sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== dependencies: asynckit "^0.4.0" combined-stream "^1.0.6" @@ -6893,6 +7482,13 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" @@ -7019,15 +7615,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793" - integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -https-proxy-agent@^2.2.3: +https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -8555,14 +9143,10 @@ lead@^1.0.0: dependencies: flush-write-stream "^1.0.2" -"leaflet.polylinemeasure@github:ppete2/Leaflet.PolylineMeasure": - version "1.0.0" - resolved "https://codeload.github.com/ppete2/Leaflet.PolylineMeasure/tar.gz/8fed0cbe7c17dae4e1ce0852fa243d8467325a77" - -leaflet@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.5.1.tgz#9afb9d963d66c870066b1342e7a06f92840f46bf" - integrity sha512-ekM9KAeG99tYisNBg0IzEywAlp0hYI5XRipsqRXyRTeuU8jcuntilpp+eFf5gaE0xubc9RuSNIVtByEKwqFV0w== +leaflet@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" + integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== left-pad@^1.3.0: version "1.3.0" @@ -8824,6 +9408,11 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" +lodash-es@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -9036,7 +9625,7 @@ lodash@4.17.14: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -9462,6 +10051,21 @@ meow@^4.0.0: redent "^2.0.0" trim-newlines "^2.0.0" +meow@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" + integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + yargs-parser "^10.0.0" + merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" @@ -9757,7 +10361,14 @@ moment-timezone@^0.5.23: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@>=1.6.0, moment@^2.17.1: +moment-timezone@^0.5.27: + version "0.5.28" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" + integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@>=1.6.0, moment@^2.17.1, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== @@ -11949,6 +12560,11 @@ postcss-value-parser@^4.0.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== +postcss-value-parser@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" + integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== + postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" @@ -12296,6 +12912,11 @@ qs@^6.3.0, qs@^6.4.0, qs@^6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.9.1: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -12343,6 +12964,11 @@ qw@~1.0.1: resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ= +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -12410,14 +13036,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-addons-shallow-compare@^15.4.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f" - integrity sha1-GYoAuR/DdiPbZKKP0XtZa6NicC8= - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" - react-bootstrap@^0.32.1: version "0.32.4" resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.32.4.tgz#8efc4cbfc4807215d75b7639bee0d324c8d740d1" @@ -12483,14 +13101,14 @@ react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== -react-leaflet@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-2.4.0.tgz#9cef74e14aeeb094bfe662b46de1682623c090ea" - integrity sha512-ex9MAz2cUAmdUucsjv180OYszdqxHIyEwzWAuMOOuxE7yUmRscxZKR5h0f+vG4shR+SekZYUBk0+gCv8apRADQ== +react-leaflet@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-2.6.1.tgz#e5d6514d2358c51a28e614b8a0ddf8dca56e7179" + integrity sha512-pqXQegdX4EL4R+zL33J92aLUcznp5TksNdW6CerwEwmi5m6CHt5k2YTkvyPVroZPA7pLAPB3ZAjmLLEmZh7yLw== dependencies: - "@babel/runtime" "^7.4.5" - fast-deep-equal "^2.0.1" - hoist-non-react-statics "^3.3.0" + "@babel/runtime" "^7.7.6" + fast-deep-equal "^3.1.1" + hoist-non-react-statics "^3.3.1" warning "^4.0.3" react-lifecycles-compat@^3.0.4: @@ -12553,6 +13171,17 @@ react-resize-detector@^2.1.0: prop-types "^15.6.0" resize-observer-polyfill "^1.5.0" +react-resize-detector@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.1.tgz#8982b74c3e1cf949afaa3c41050458c87b033982" + integrity sha512-ZfPMBPxXi0o3xox42MIEtz84tPSVMW9GgwLHYvjVXlFM+OkNzbeEtpVSV+mSTJmk4Znwomolzt35zHN9LNBQMQ== + dependencies: + lodash "^4.17.15" + lodash-es "^4.17.15" + prop-types "^15.7.2" + raf-schd "^4.0.2" + resize-observer-polyfill "^1.5.1" + react-router@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f" @@ -13324,7 +13953,7 @@ require-relative@^0.8.7: resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4= -resize-observer-polyfill@^1.5.0: +resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== @@ -13722,6 +14351,11 @@ sha@^3.0.0: dependencies: graceful-fs "^4.1.2" +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + shasum@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" @@ -14369,6 +15003,52 @@ style-loader@^0.13.0: dependencies: loader-utils "^1.0.2" +styled-components@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.0.1.tgz#57782a6471031abefb2db5820a1876ae853bc619" + integrity sha512-E0xKTRIjTs4DyvC1MHu/EcCXIj6+ENCP8hP01koyoADF++WdBUOrSGwU1scJRw7/YaYOhDvvoad6VlMG+0j53A== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^0.8.3" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + +styled-icons@^9.1.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/styled-icons/-/styled-icons-9.5.0.tgz#d1f6c8dcc3ea81c3d30fe20dfda5d39a8afc593c" + integrity sha512-nby51U6+SRY5010P15XogBZubrqblqXTV50+lA/xBkC1QrTap1BIgCvoJwH9FF5kYI+LKFl/xeco/drwqIR2Pw== + dependencies: + "@styled-icons/boxicons-logos" "^9.4.1" + "@styled-icons/boxicons-regular" "^9.4.1" + "@styled-icons/boxicons-solid" "^9.4.1" + "@styled-icons/crypto" "^9.4.1" + "@styled-icons/entypo" "^9.5.0" + "@styled-icons/entypo-social" "^9.5.0" + "@styled-icons/evil" "^9.4.1" + "@styled-icons/fa-brands" "^9.4.1" + "@styled-icons/fa-regular" "^9.4.1" + "@styled-icons/fa-solid" "^9.4.1" + "@styled-icons/feather" "^9.4.1" + "@styled-icons/foundation" "^9.5.0" + "@styled-icons/heroicons-outline" "^9.5.0" + "@styled-icons/heroicons-solid" "^9.5.0" + "@styled-icons/icomoon" "^9.4.1" + "@styled-icons/material" "^9.4.1" + "@styled-icons/octicons" "^9.4.1" + "@styled-icons/open-iconic" "^9.5.0" + "@styled-icons/remix-fill" "^9.4.1" + "@styled-icons/remix-line" "^9.4.1" + "@styled-icons/styled-icon" "^9.4.1" + "@styled-icons/typicons" "^9.4.1" + "@styled-icons/zondicons" "^9.5.0" + tslib "^1.9.3" + stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -14407,7 +15087,7 @@ supports-color@^3.1.0, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -14565,7 +15245,7 @@ throat@^4.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= -throttle-debounce@^2.0.1: +throttle-debounce@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5" integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg== @@ -14846,6 +15526,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tslib@^1.9.3: + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -14863,49 +15548,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turf-along@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-along/-/turf-along-3.0.12.tgz#e622bde7a4bd138c09647d4b14aa0ea700485de6" - integrity sha1-5iK956S9E4wJZH1LFKoOpwBIXeY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-bearing@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bearing/-/turf-bearing-3.0.12.tgz#65f609dd850e7364c7771aa6ded87b0e1917fd20" - integrity sha1-ZfYJ3YUOc2THdxqm3th7DhkX/SA= - dependencies: - turf-invariant "^3.0.12" - -turf-destination@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-destination/-/turf-destination-3.0.12.tgz#7dd6fbf97e86f831a26c83ef2d5a2f8d1d8a6de2" - integrity sha1-fdb7+X6G+DGibIPvLVovjR2KbeI= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-distance/-/turf-distance-3.0.12.tgz#fb97b8705facd993b145e014b41862610eeca449" - integrity sha1-+5e4cF+s2ZOxReAUtBhiYQ7spEk= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-helpers@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-helpers/-/turf-helpers-3.0.12.tgz#dd4272e74b3ad7c96eecb7ae5c57fe8eca544b7b" - integrity sha1-3UJy50s618lu7LeuXFf+jspUS3s= - -turf-invariant@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" - integrity sha1-O5UlOVOZHr2WLdNdT2cEwofejr4= - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -15420,7 +16062,7 @@ velocity-animate@^1.4.0: resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-1.5.2.tgz#5a351d75fca2a92756f5c3867548b873f6c32105" integrity sha512-m6EXlCAMetKztO1ppBhGU1/1MR3IiEevO6ESq6rcrSQ3Q77xYSW13jkfXW88o4xMrkXJhy/U7j4wFR/twMB0Eg== -velocity-react@^1.3.3: +velocity-react@^1.3.3, velocity-react@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/velocity-react/-/velocity-react-1.4.3.tgz#63e41d92e334d5a3bea8b2fa02ee170f62ef4d36" integrity sha512-zvefGm85A88S3KdF9/dz5vqyFLAiwKYlXGYkHH2EbXl+CZUD1OT0a0aS1tkX/WXWTa/FUYqjBaAzAEFYuSobBQ== @@ -15946,6 +16588,13 @@ yamljs@^0.3.0: argparse "^1.0.7" glob "^7.0.5" +yargs-parser@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"