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-config.yml b/example-config.yml index 6588b401f..18d6b4fc2 100644 --- a/example-config.yml +++ b/example-config.yml @@ -16,14 +16,30 @@ api: # name: Oregon Zoo, Portland, OR ### The persistence setting is used to enable the storage of places (home, work), -### recent searches/places, user overrides, and favorite stops. Currently the -### only strategy is localStorage (which is used by default). It also must be -### enabled to show the stored locations (see above). -### TODO: add another server-based strategy +### recent searches/places, user overrides, and favorite stops. +### Pick the strategy that best suits your needs. +### +### If you do not require remote storage of preferences, +### then use the localStorage strategy outlined below (which is used by default). +### The localStorage strategy will use the browser application storage. +### It must be enabled to show the stored locations (see above). persistence: enabled: true strategy: localStorage +### If using the OTP Middleware to store user profiles +### with Auth0 as the authentication mechanism, +### then use the otp_middleware strategy below instead: +# persistence: +# enabled: true +# strategy: otp_middleware +# auth0: +# domain: your-auth0-domain +# clientId: your-auth0-client-id +# otp_middleware: +# apiBaseUrl: https://otp-middleware.example.com +# apiKey: your-middleware-api-key + map: initLat: 45.52 initLon: -122.682 diff --git a/example.js b/example.js index b29df8943..41142d151 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' @@ -11,24 +12,35 @@ 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 { Grid, Row, Col } from 'react-bootstrap' // import OTP-RR components import { - DefaultSearchForm, - ErrorMessage, + DefaultMainPanel, + DesktopNav, + Map, MobileMain, - NarrativeRoutingResults, ResponsiveWebapp, - Map, - ViewerContainer, - AppMenu, - createOtpReducer + createOtpReducer, + createUserReducer } 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: { @@ -57,6 +69,7 @@ if (process.env.NODE_ENV === 'development') { const store = createStore( combineReducers({ otp: createOtpReducer(otpConfig), + user: createUserReducer(), router: connectRouter(history) }), compose(applyMiddleware(...middleware)) @@ -68,28 +81,12 @@ class OtpRRExample extends Component { /** desktop view **/ const desktopView = (
- - - -
- -
-
OpenTripPlanner
-
-
-
+ - - - -
- -
-
+ - @@ -100,7 +97,12 @@ class OtpRRExample extends Component { /** mobile view **/ const mobileView = ( - )} title={(
OpenTripPlanner
)} /> + } + title={
OpenTripPlanner
} + /> ) /** the main webapp **/ @@ -108,6 +110,7 @@ class OtpRRExample extends Component { ) } @@ -115,15 +118,18 @@ class OtpRRExample extends Component { // render the app render( - - { /** + ( + + { /** * If not using router history, simply include OtpRRExample here: * e.g. * */ - } - + } + + + ) + , - , document.getElementById('root') ) diff --git a/lib/actions/api.js b/lib/actions/api.js index 705822ecb..1ac0b0c22 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1,20 +1,24 @@ /* 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' +import { getSecureFetchOptions } from '../util/middleware' + 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') @@ -79,7 +83,9 @@ function getActiveItinerary (otpState) { */ export function routingQuery (searchId = null) { return async function (dispatch, getState) { - const otpState = getState().otp + const state = getState() + const otpState = state.otp + const isNewSearch = !searchId if (isNewSearch) searchId = randId() const routingType = otpState.currentQuery.routingType @@ -93,7 +99,7 @@ export function routingQuery (searchId = null) { // fetch a realtime route const query = constructRoutingQuery(otpState) - fetch(query) + fetch(query, getOtpFetchOptions(state)) .then(getJsonAndCheckResponse) .then(json => { dispatch(routingResponse({ response: json, searchId })) @@ -124,8 +130,29 @@ export function routingQuery (searchId = null) { if (isNewSearch || params.ui_activeSearch !== searchId) { dispatch(updateOtpUrlParams(otpState, searchId)) } - // also fetch a non-realtime route - fetch(constructRoutingQuery(otpState, true)) + + // Also fetch a non-realtime route. + // + // FIXME: The statement below may no longer apply with future work + // involving realtime info embedded in the OTP response. + // (That action records an entry again in the middleware.) + // For users who opted in to store trip request history, + // to avoid recording the same trip request twice in the middleware, + // only add the user Authorization token to the request + // when querying the non-realtime route. + // + // The advantage of using non-realtime route is that the middleware will be able to + // record and provide the theoretical itinerary summary without having to query OTP again. + // FIXME: Interestingly, and this could be from a side effect elsewhere, + // when a logged-in user refreshes the page, the trip request in the URL is not recorded again + // (state.user stays unpopulated until after this function is called). + // + const { user } = state + const storeTripHistory = user && + user.loggedInUser && + user.loggedInUser.storeTripHistory + + fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, storeTripHistory)) .then(getJsonAndCheckResponse) .then(json => { // FIXME: This is only performed when ignoring realtimeupdates currently, just @@ -148,6 +175,39 @@ function getJsonAndCheckResponse (res) { return res.json() } +/** + * This method determines the fetch options (including API key and Authorization headers) for the OTP API. + * - If the OTP server is not the middleware server (standalone OTP server), + * an empty object is returned. + * - If the OTP server is the same as the middleware server, + * then an object is returned with the following: + * - A middleware API key, if it has been set in the configuration (it is most likely required), + * - An Auth0 accessToken, when includeToken is true and a user is logged in (userState.loggedInUser is not null). + * This method assumes JSON request bodies.) + */ +function getOtpFetchOptions (state, includeToken = false) { + let apiBaseUrl, apiKey, token + + const { api, persistence } = state.otp.config + if (persistence && persistence.otp_middleware) { + ({ apiBaseUrl, apiKey } = persistence.otp_middleware) + } + + const isOtpServerSameAsMiddleware = apiBaseUrl === api.host + if (isOtpServerSameAsMiddleware) { + if (includeToken && state.user) { + const { accessToken, loggedInUser } = state.user + if (accessToken && loggedInUser) { + token = accessToken + } + } + + return getSecureFetchOptions(token, apiKey) + } else { + return {} + } +} + function constructRoutingQuery (otpState, ignoreRealtimeUpdates) { const { config, currentQuery } = otpState const routingType = currentQuery.routingType @@ -937,7 +997,7 @@ export function setUrlSearch (params, replaceCurrent = false) { * is set correctly. Leaves any other existing URL parameters (e.g., UI) unchanged. */ export function updateOtpUrlParams (otpState, searchId) { - const otpParams = getRoutingParams(otpState, true) + const otpParams = getRoutingParams(otpState) return function (dispatch, getState) { const params = {} // Get all OTP-specific params, which will be retained unchanged in the URL diff --git a/lib/actions/auth.js b/lib/actions/auth.js new file mode 100644 index 000000000..cf5478e0f --- /dev/null +++ b/lib/actions/auth.js @@ -0,0 +1,59 @@ +import { push } from 'connected-react-router' + +import { setPathBeforeSignIn } from '../actions/user' + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * when a new access token could not be retrieved. + * @param {Error} err + * @param {AccessTokenRequestOptions} options + */ +export function showAccessTokenError (err, options) { + return function (dispatch, getState) { + // TODO: improve this. + console.error('Failed to retrieve access token: ', err) + } +} + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * when signing-in fails for some reason. + * @param {Error} err + */ +export function showLoginError (err) { + return function (dispatch, getState) { + // TODO: improve this. + if (err) dispatch(push('/oops')) + } +} + +/** + * This function is called by the Auth0Provider component, with the described parameter(s), + * after the user signs in. + * @param {Object} appState The state that was stored when calling useAuth().login(). + */ +export function processSignIn (appState) { + return function (dispatch, getState) { + if (appState && appState.urlHash) { + // At this stage after login, Auth0 has already redirected to /signedin (Auth0-whitelisted) + // which shows the AfterLoginScreen. + // + // Here, we save the URL hash prior to login (contains a combination of itinerary search, stop/trip view, etc.), + // so that the AfterLoginScreen can redirect back there when logged-in user info is fetched. + // (For routing, it is easier to deal with the path without the hash sign.) + const hashIndex = appState.urlHash.indexOf('#') + const urlHashWithoutHash = hashIndex >= 0 + ? appState.urlHash.substr(hashIndex + 1) + : '/' + dispatch(setPathBeforeSignIn(urlHashWithoutHash)) + } else if (appState && appState.returnTo) { + // TODO: Handle other after-login situations. + // Note that when redirecting from a login-protected (e.g. account) page while logged out, + // then returnTo is set by Auth0 to this object format: + // { + // pathname: "/" + // query: { ... } + // } + } + } +} diff --git a/lib/actions/form.js b/lib/actions/form.js index 296579772..023e5ff17 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 planParamsToQuery(planParams, getState().otp.config) .then(query => dispatch(setQueryParam(query, searchId))) @@ -82,10 +81,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 @@ -104,7 +104,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 ) { @@ -116,8 +116,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 c4f386244..27cca104a 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -1,22 +1,25 @@ +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 + * Wrapper function for history#push (or, if specified, replace, etc.) + * that preserves the current search or, if * replaceSearch is provided (including an empty string), replaces the search * when routing to a new URL path. * @param {[type]} url path to route to * @param {string} replaceSearch optional search string to replace current one + * @param {func} routingMethod the connected-react-router method to execute (defaults to push). */ -export function routeTo (url, replaceSearch) { +export function routeTo (url, replaceSearch, routingMethod = push) { return function (dispatch, getState) { // Get search to preserve when routing to new path. const { router } = getState() @@ -27,7 +30,7 @@ export function routeTo (url, replaceSearch) { } else { path = `${path}${search}` } - dispatch(push(path)) + dispatch(routingMethod(path)) } } @@ -109,7 +112,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/actions/user.js b/lib/actions/user.js new file mode 100644 index 000000000..f306af6ae --- /dev/null +++ b/lib/actions/user.js @@ -0,0 +1,107 @@ +import { createAction } from 'redux-actions' + +import { addUser, fetchUser, updateUser } from '../util/middleware' +import { isNewUser } from '../util/user' + +const setCurrentUser = createAction('SET_CURRENT_USER') +export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN') + +function getStateForNewUser (auth0User) { + return { + auth0UserId: auth0User.sub, + email: auth0User.email, + hasConsentedToTerms: false, // User must agree to terms. + isEmailVerified: auth0User.email_verified, + notificationChannel: 'email', + phoneNumber: '', + recentLocations: [], + savedLocations: [], + storeTripHistory: false // User must opt in. + } +} + +/** + * Fetches user preferences to state.user, or set initial values under state.user if no user has been loaded. + */ +export function fetchOrInitializeUser (auth) { + return async function (dispatch, getState) { + const { otp } = getState() + const { accessToken, user } = auth + + try { + const result = await fetchUser( + otp.config.persistence.otp_middleware, + accessToken + ) + + // Beware! On AWS API gateway, if a user is not found in the middleware + // (e.g. they just created their Auth0 password but have not completed the account setup form yet), + // the call above will return, for example: + // { + // status: 'success', + // data: { + // "result": "ERR", + // "message": "No user with id=000000 found.", + // "code": 404, + // "detail": null + // } + // } + // + // The same call to a middleware instance that is not behind an API gateway + // will return: + // { + // status: 'error', + // message: 'Error get-ing user...' + // } + // TODO: Improve AWS response. + + const resultData = result.data + const isNewAccount = result.status === 'error' || (resultData && resultData.result === 'ERR') + + if (!isNewAccount) { + // TODO: Move next line somewhere else. + if (resultData.savedLocations === null) resultData.savedLocations = [] + dispatch(setCurrentUser({ accessToken, user: resultData })) + } else { + dispatch(setCurrentUser({ accessToken, user: getStateForNewUser(user) })) + } + } catch (error) { + // TODO: improve error handling. + alert(`An error was encountered:\n${error}`) + } + } +} + +/** + * Updates (or creates) a user entry in the middleware, + * then, if that was successful, updates the redux state with that user. + */ +export function createOrUpdateUser (userData) { + return async function (dispatch, getState) { + const { otp, user } = getState() + const { otp_middleware: otpMiddleware = null } = otp.config.persistence + + if (otpMiddleware) { + const { accessToken, loggedInUser } = user + + let result + if (isNewUser(loggedInUser)) { + result = await addUser(otpMiddleware, accessToken, userData) + } else { + result = await updateUser(otpMiddleware, accessToken, userData) + } + + // TODO: improve the UI feedback messages for this. + if (result.status === 'success' && result.data) { + // Update application state with the user entry as saved + // (as returned) by the middleware. + const userData = result.data + dispatch(setCurrentUser({ accessToken, user: userData })) + + alert('Your preferences have been saved.') + } else { + alert(`An error was encountered:\n${JSON.stringify(result)}`) + } + } + } +} 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/desktop-nav.js b/lib/components/app/desktop-nav.js new file mode 100644 index 000000000..558f166aa --- /dev/null +++ b/lib/components/app/desktop-nav.js @@ -0,0 +1,81 @@ +import React from 'react' +import { Nav, Navbar } from 'react-bootstrap' +import { connect } from 'react-redux' + +import NavLoginButtonAuth0 from '../user/nav-login-button-auth0.js' +import { accountLinks, getAuth0Config } from '../../util/auth' +import { DEFAULT_APP_TITLE } from '../../util/constants' +import AppMenu from './app-menu' + +/** + * The desktop navigation bar, featuring a `branding` logo or a `title` text + * defined in config.yml, and a sign-in button/menu with account links. + * + * The `branding` and `title` parameters in config.yml are handled + * and shown in this order in the navigation bar: + * 1. If `branding` is defined, it is shown, and no title is displayed. + * 2. If `branding` is not defined but if `title` is, then `title` is shown. + * 3. If neither is defined, just show 'OpenTripPlanner' (DEFAULT_APP_TITLE). + * + * TODO: merge with the mobile navigation bar. + */ +const DesktopNav = ({ otpConfig }) => { + const { branding, persistence, title = DEFAULT_APP_TITLE } = otpConfig + const showLogin = Boolean(getAuth0Config(persistence)) + + // Display branding and title in the order as described in the class summary. + let brandingOrTitle + if (branding) { + brandingOrTitle = ( +
+ ) + } else { + brandingOrTitle = ( +
{title}
+ ) + } + + return ( + + + + {/* TODO: Reconcile CSS class and inline style. */} +
+ +
+ + {brandingOrTitle} + +
+
+ + {showLogin && ( + + + + )} +
+ ) +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { + otpConfig: state.otp.config + } +} + +const mapDispatchToProps = { +} + +export default connect(mapStateToProps, mapDispatchToProps)(DesktopNav) 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 92d98be1f..4ab454a66 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -1,20 +1,29 @@ -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 { Auth0Provider } from 'use-auth0-hooks' import PrintLayout from './print-layout' +import * as authActions from '../../actions/auth' 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 { getAuth0Config } from '../../util/auth' +import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from '../../util/constants' +import { getActiveItinerary, getTitle } from '../../util/state' +import AfterSignInScreen from '../user/after-signin-screen' +import BeforeSignInScreen from '../user/before-signin-screen' +import UserAccountScreen from '../user/user-account-screen' +import withLoggedInUserSupport from '../user/with-logged-in-user-support' + +const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { static propTypes = { @@ -29,7 +38,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 +64,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 }) @@ -159,13 +168,38 @@ const mapDispatchToProps = { const history = createHashHistory() const WebappWithRouter = withRouter( - connect(mapStateToProps, mapDispatchToProps)(ResponsiveWebapp) + withLoggedInUserSupport( + connect(mapStateToProps, mapDispatchToProps)(ResponsiveWebapp) + ) ) -class RouterWrapper extends Component { +/** + * The routing component for the application. + * This is the top-most "standard" component, + * and we initialize the Auth0Provider here + * so that Auth0 services are available everywhere. + */ +class RouterWrapperWithAuth0 extends Component { + /** + * Combine the router props with the other props that get + * passed to the exported component. This way, it is possible for + * the PrintLayout, UserAccountScreen, and other components to + * receive the LegIcon or other needed props. + */ + _combineProps = routerProps => { + return { ...this.props, ...routerProps } + } + render () { - const { routerConfig } = this.props - return ( + const { + auth0Config, + processSignIn, + routerConfig, + showAccessTokenError, + showLoginError + } = this.props + + const router = ( @@ -191,13 +225,29 @@ class RouterWrapper extends Component { ]} render={() => } /> + { + const props = this._combineProps(routerProps) + return + }} + /> + { + const props = this._combineProps(routerProps) + return + }} + /> { - // combine the router props with the other props that get - // passed to the exported component. This way it's possible for - // the PrintLayout component to receive the custom icons prop. - const props = { ...this.props, ...routerProps } + component={routerProps => { + const props = this._combineProps(routerProps) return }} /> @@ -209,7 +259,38 @@ class RouterWrapper extends Component {
) + + return ( + auth0Config + ? ( + + {router} + + ) + : router + ) } } -const mapStateToWrapperProps = (state, ownProps) => ({ routerConfig: state.otp.config.reactRouter }) -export default connect(mapStateToWrapperProps)(RouterWrapper) + +const mapStateToWrapperProps = (state, ownProps) => ({ + auth0Config: getAuth0Config(state.otp.config.persistence), + routerConfig: state.otp.config.reactRouter +}) + +const mapWrapperDispatchToProps = { + processSignIn: authActions.processSignIn, + showAccessTokenError: authActions.showAccessTokenError, + showLoginError: authActions.showLoginError +} + +export default connect(mapStateToWrapperProps, mapWrapperDispatchToProps)(RouterWrapperWithAuth0) 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 ebd0d35a1..5398024df 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-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 { Button } from 'react-bootstrap' import { connect } from 'react-redux' import { mergeMessages } from '../../util/messages' -import { isNotDefaultQuery } from '../../util/query' class SettingsPreview extends Component { static propTypes = { @@ -12,7 +12,6 @@ class SettingsPreview extends Component { caret: PropTypes.string, compressed: PropTypes.bool, editButtonText: PropTypes.element, - icons: PropTypes.object, showCaret: PropTypes.bool, onClick: PropTypes.func, @@ -33,7 +32,7 @@ class SettingsPreview extends Component { const { caret, config, query, editButtonText } = this.props const messages = mergeMessages(SettingsPreview.defaultProps, 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..f86516861 --- /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 { + stop: 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..3ce357508 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -1,24 +1,118 @@ +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 { updateOverlayVisibility } from '../../actions/config' +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 { + /** + * 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) + } + } + } + + onMapClick = (e) => { + this.props.setMapPopupLocationAndGeocode(e) + } + + onPopupClosed = () => { + this.props.setMapPopupLocation({ location: null }) + } + + onSetLocationFromPopup = (payload) => { + const { setLocation, setMapPopupLocation } = this.props + setMapPopupLocation({ location: null }) + setLocation(payload) + } + + componentDidUpdate (prevProps) { + // Check if any overlays should be toggled due to mode change + this._handleQueryChange(prevProps.query, this.props.query) + } + render () { const { bikeRentalQuery, @@ -26,58 +120,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 + } + })} + + ) } } @@ -85,10 +203,16 @@ class DefaultMap extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { + const overlays = state.otp.config.map && state.otp.config.map.overlays + ? state.otp.config.map.overlays + : [] return { bikeRentalStations: state.otp.overlay.bikeRental.stations, carRentalStations: state.otp.overlay.carRental.stations, mapConfig: state.otp.config.map, + mapPopupLocation: state.otp.ui.mapPopupLocation, + overlays, + query: state.otp.currentQuery, vehicleRentalStations: state.otp.overlay.vehicleRental.stations } } @@ -96,6 +220,10 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { bikeRentalQuery, carRentalQuery, + setLocation, + setMapPopupLocation, + setMapPopupLocationAndGeocode, + updateOverlayVisibility, 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/navigation-bar.js b/lib/components/mobile/navigation-bar.js index d861d1e28..cac315e1c 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -1,11 +1,13 @@ -import React, { Component } from 'react' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import React, { Component } from 'react' import { Navbar } from 'react-bootstrap' import FontAwesome from 'react-fontawesome' +import { connect } from 'react-redux' -import AppMenu from '../app/app-menu' import { setMobileScreen } from '../../actions/ui' +import AppMenu from '../app/app-menu' +import NavLoginButtonAuth0 from '../../components/user/nav-login-button-auth0' +import { accountLinks, getAuth0Config } from '../../util/auth' class MobileNavigationBar extends Component { static propTypes = { @@ -24,7 +26,13 @@ class MobileNavigationBar extends Component { } render () { - const { showBackButton, headerAction, headerText, title } = this.props + const { + auth0Config, + headerAction, + headerText, + showBackButton, + title + } = this.props return ( @@ -50,6 +58,19 @@ class MobileNavigationBar extends Component {
{headerAction}
)} + + {/** + * HACK: Normally, NavLoginButtonAuth0 should be inside a