From 3c3c54fd479c0395f9ca295027129ea596f06864 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 14:22:58 +0000 Subject: [PATCH 1/9] parse a ropped JSON file --- frontend/src/Sidebar/index.jsx | 7 ++++++- frontend/src/Sidebar/restoreStateFromFile.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 frontend/src/Sidebar/restoreStateFromFile.js diff --git a/frontend/src/Sidebar/index.jsx b/frontend/src/Sidebar/index.jsx index 36d7764..e947d2d 100644 --- a/frontend/src/Sidebar/index.jsx +++ b/frontend/src/Sidebar/index.jsx @@ -5,11 +5,16 @@ import FactorContainer from './FactorContainer' import BigButton from './BigButton' import FactorList from './FactorList' import { TravelTimeQuery } from '../travelTimeQuery.js' +import { restoreStateFromFile } from './restoreStateFromFile.js' import './sidebar.css' export default function SidebarContent(){ return ( -
+
{ e.stopPropagation(); e.preventDefault() } } + onDragOver={ e => { e.stopPropagation(); e.preventDefault() } } + onDrop={restoreStateFromFile} + >
×
diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js new file mode 100644 index 0000000..2550cb4 --- /dev/null +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -0,0 +1,11 @@ +export function restoreStateFromFile(fileDropEvent){ + fileDropEvent.stopPropagation() + fileDropEvent.preventDefault() + // only handle one at a time + let file = fileDropEvent.dataTransfer.files[0] + console.log(file) + if( ! file.type == 'application/json' ) { return } + let reader = new FileReader() + reader.onload = e => { console.log(JSON.parse(e.target.result)) } + reader.readAsText(file) +} \ No newline at end of file From 57d4ccf9509e88903c6db54ad083987eab9b22b1 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 15:35:03 +0000 Subject: [PATCH 2/9] add distinct time ranges --- frontend/src/Sidebar/index.jsx | 8 +++- frontend/src/Sidebar/restoreStateFromFile.js | 39 ++++++++++++++++---- frontend/src/spatialData.js | 1 + frontend/src/timeRange.js | 2 +- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/frontend/src/Sidebar/index.jsx b/frontend/src/Sidebar/index.jsx index e947d2d..85d63fc 100644 --- a/frontend/src/Sidebar/index.jsx +++ b/frontend/src/Sidebar/index.jsx @@ -9,11 +9,15 @@ import { restoreStateFromFile } from './restoreStateFromFile.js' import './sidebar.css' export default function SidebarContent(){ + const { data, logActivity } = useContext(DataContext) return (
{ e.stopPropagation(); e.preventDefault() } } onDragOver={ e => { e.stopPropagation(); e.preventDefault() } } - onDrop={restoreStateFromFile} + onDrop={ event => { + restoreStateFromFile(event,data) + .then( logActivity('state restored from file') ) // not working? + } } > @@ -45,7 +49,7 @@ function Results(){ setIsFetchingData(true) data.fetchAllResults().then( () => { setIsFetchingData(false) - setResults(data.travelTimeQueries) + setResults(data.travelTimeQueries) } ) }}> Submit Query diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index 2550cb4..305988f 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,11 +1,36 @@ -export function restoreStateFromFile(fileDropEvent){ + +const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)/ + +export async function restoreStateFromFile(fileDropEvent,stateData){ fileDropEvent.stopPropagation() fileDropEvent.preventDefault() - // only handle one at a time + + // only handle one file at a time let file = fileDropEvent.dataTransfer.files[0] - console.log(file) - if( ! file.type == 'application/json' ) { return } - let reader = new FileReader() - reader.onload = e => { console.log(JSON.parse(e.target.result)) } - reader.readAsText(file) + + // TODO: handle CSV + if( file.type != 'application/json' ) { return } + + return file.text() + .then( text => JSON.parse(text) ) + .then( data => { + // should be a list of objects each with a URI property + let URIs = data.map( r => r.URI.match(URIpattern).groups ) + console.log(URIs) + + distinctPairs(URIs,'startTime','endTime') + .forEach( ({startTime,endTime}) => { + let timeRange = stateData.createTimeRange() + timeRange.setStartTime(startTime) + timeRange.setEndTime(endTime) + } ) + } ) +} + +function distinctPairs(list,prop1,prop2){ + let distinctKeys = new Set( list.map(o => `${o[prop1]} | ${o[prop2]}`) ) + return [...distinctKeys].map( k => { + let vals = k.split(' | ') + return { [prop1]: vals[0], [prop2]: vals[1] } + } ) } \ No newline at end of file diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 359cc5c..2e6a919 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -38,6 +38,7 @@ export class SpatialData { let tr = new TimeRange(this) this.#factors.push(tr) tr.activate() + return tr } createDateRange(){ let dr = new DateRange(this) diff --git a/frontend/src/timeRange.js b/frontend/src/timeRange.js index bedf860..f269172 100644 --- a/frontend/src/timeRange.js +++ b/frontend/src/timeRange.js @@ -25,7 +25,7 @@ export class TimeRange extends Factor { return } static parseTime(timeString){ - let match = timeString.match(/^(?\d{2})$/) + let match = timeString.match(/^(?\d{1,2})$/) if(match){ let {hours} = match.groups return new Date(1970, 1, 1, parseInt(hours)) From d7f75cf6105bd95e56330d54219cd06211c431d1 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 15:43:12 +0000 Subject: [PATCH 3/9] handle date ranges --- frontend/src/Sidebar/restoreStateFromFile.js | 8 +++++++- frontend/src/spatialData.js | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index 305988f..6d306dd 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,5 +1,5 @@ -const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)/ +const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})/ export async function restoreStateFromFile(fileDropEvent,stateData){ fileDropEvent.stopPropagation() @@ -24,6 +24,12 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ timeRange.setStartTime(startTime) timeRange.setEndTime(endTime) } ) + distinctPairs(URIs,'startDate','endDate') + .forEach( ({startDate,endDate}) => { + let dateRange = stateData.createDateRange() + dateRange.setStartDate(new Date(Date.parse(startDate))) + dateRange.setEndDate(new Date(Date.parse(endDate))) + } ) } ) } diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 2e6a919..050cab6 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -44,6 +44,7 @@ export class SpatialData { let dr = new DateRange(this) this.#factors.push(dr) dr.activate() + return dr } createDays(){ let days = new Days(this) From 2d20eaf01f7de52346e863e2f7f555b8c5589a93 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 15:49:58 +0000 Subject: [PATCH 4/9] parse distinct corridors --- frontend/src/Sidebar/restoreStateFromFile.js | 10 +++++++++- frontend/src/spatialData.js | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index 6d306dd..9c01cb1 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -16,8 +16,16 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ .then( data => { // should be a list of objects each with a URI property let URIs = data.map( r => r.URI.match(URIpattern).groups ) - console.log(URIs) + distinctPairs(URIs,'startNode','endNode') + .forEach( ({startNode,endNode}) => { + console.log('unhandled corridor nodes',startNode,endNode) + /* TODO do soemthing like + let corridor = stateData.createCorridor() + corridor.addIntersection(startNode) + corridor.addIntersection(endNode) + */ + } ) distinctPairs(URIs,'startTime','endTime') .forEach( ({startTime,endTime}) => { let timeRange = stateData.createTimeRange() diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 050cab6..4094017 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -33,6 +33,7 @@ export class SpatialData { let corridor = new Corridor(this) this.#factors.push(corridor) corridor.activate() + return corridor } createTimeRange(){ let tr = new TimeRange(this) From c5bd6e6f78f80f267abef6fce62d3f29eed4c4a6 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 16:10:12 +0000 Subject: [PATCH 5/9] update holiday inclusion --- frontend/src/Sidebar/restoreStateFromFile.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index 9c01cb1..d451f7b 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,5 +1,5 @@ -const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})/ +const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})\/(?true|false)\/(?\d+)/ export async function restoreStateFromFile(fileDropEvent,stateData){ fileDropEvent.stopPropagation() @@ -20,7 +20,7 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ distinctPairs(URIs,'startNode','endNode') .forEach( ({startNode,endNode}) => { console.log('unhandled corridor nodes',startNode,endNode) - /* TODO do soemthing like + /* TODO do something like - but more complicated! let corridor = stateData.createCorridor() corridor.addIntersection(startNode) corridor.addIntersection(endNode) @@ -38,10 +38,21 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ dateRange.setStartDate(new Date(Date.parse(startDate))) dateRange.setEndDate(new Date(Date.parse(endDate))) } ) + // holiday inclusion + let holidays = new Set(URIs.map(uri => uri.holidays)) + if(holidays.has('true') && holidays.has('false')){ + stateData.includeAndExcludeHolidays() + }else if(holidays.has('true')){ + stateData.includeHolidays() + }else{ + stateData.excludeHolidays() + } } ) } -function distinctPairs(list,prop1,prop2){ +// get distinct value pairs from a list of objects by their property names +// all are strings +function distinctPairs(list, prop1, prop2){ let distinctKeys = new Set( list.map(o => `${o[prop1]} | ${o[prop2]}`) ) return [...distinctKeys].map( k => { let vals = k.split(' | ') From cba954cbb78e5f66cda74610a0eb7639cb6b4327 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 16:34:57 +0000 Subject: [PATCH 6/9] restore DoW selections --- frontend/src/Sidebar/restoreStateFromFile.js | 8 +++++++- frontend/src/days.js | 5 +++++ frontend/src/spatialData.js | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index d451f7b..cb24715 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,4 +1,3 @@ - const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})\/(?true|false)\/(?\d+)/ export async function restoreStateFromFile(fileDropEvent,stateData){ @@ -47,6 +46,13 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ }else{ stateData.excludeHolidays() } + // days of week + // TODO: drop the default selection? + [... new Set(URIs.map(uri=>uri.dow))].forEach( dows => { + let daysFactor = stateData.createDays() + let days = dows.split('').map(Number) + daysFactor.setFromSet(new Set(days)) + } ) } ) } diff --git a/frontend/src/days.js b/frontend/src/days.js index 6fd06f2..933f921 100644 --- a/frontend/src/days.js +++ b/frontend/src/days.js @@ -38,6 +38,11 @@ export class Days extends Factor { hasDay(number){ return this.#days.has(parseInt(number)) } + setFromSet(dowSet){ + const validDays = new Set([...weekday,...weekend]) + let validDowSet = new Set([...dowSet].filter(v => validDays.has(v))) + this.#days = validDowSet + } get name(){ if(this.#days.size == 7){ return 'all days' diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 4094017..e3daadc 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -51,6 +51,7 @@ export class SpatialData { let days = new Days(this) this.#factors.push(days) days.activate() + return days } get segments(){ return this.corridors.flatMap( c => c.segments ) From 785770dc506b668e2e8e578b25160385868128bf Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 17:06:01 +0000 Subject: [PATCH 7/9] create endpoint for node lookup by ID --- backend/app/get_node.py | 31 ++++++++++++++++++++ backend/app/routes.py | 9 ++++++ frontend/src/Sidebar/restoreStateFromFile.js | 5 ++-- 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 backend/app/get_node.py diff --git a/backend/app/get_node.py b/backend/app/get_node.py new file mode 100644 index 0000000..c2bf72d --- /dev/null +++ b/backend/app/get_node.py @@ -0,0 +1,31 @@ +import json +from app.db import getConnection + +sql = ''' +SELECT + cg_nodes.node_id::int, + ST_AsGeoJSON(cg_nodes.geom) AS geom, + array_agg(DISTINCT InitCap(streets.st_name)) FILTER (WHERE streets.st_name IS NOT NULL) AS street_names +FROM congestion.network_nodes AS cg_nodes +JOIN here.routing_nodes_21_1 AS here_nodes USING (node_id) +JOIN here_gis.streets_att_21_1 AS streets USING (link_id) +WHERE node_id = %(node_id)s +GROUP BY + cg_nodes.node_id, + cg_nodes.geom; +''' + +# TODO code could use some tidying up +def get_node(node_id): + with getConnection() as connection: + with connection.cursor() as cursor: + cursor.execute(sql, {"node_id": node_id}) + nodes = [] + for node_id, geojson, street_names in cursor.fetchall(): + nodes.append( { + 'node_id': node_id, + 'street_names': street_names, + 'geometry': json.loads(geojson) + } ) + connection.close() + return nodes[0] if len(nodes) > 0 else {} \ No newline at end of file diff --git a/backend/app/routes.py b/backend/app/routes.py index f5f4150..4256d4c 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -4,6 +4,7 @@ from app import app from app.db import getConnection from app.get_closest_nodes import get_closest_nodes +from app.get_node import get_node from app.get_travel_time import get_travel_time from app.get_links import get_links @@ -25,6 +26,14 @@ def closest_node(longitude,latitude): return jsonify({'error': "Longitude and latitude must be decimal numbers!"}) return jsonify(get_closest_nodes(longitude,latitude)) +# test URL /node/30357505 +@app.route('/node/', methods=['GET']) +def node(node_id): + try: + node_id = int(node_id) + except: + return jsonify({'error': "node_id should be an integer"}) + return jsonify(get_node(node_id)) # test URL /link-nodes/30421154/30421153 #shell function - outputs json for use on frontend diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index cb24715..572d097 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -48,10 +48,9 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ } // days of week // TODO: drop the default selection? - [... new Set(URIs.map(uri=>uri.dow))].forEach( dows => { + [... new Set(URIs.map(uri=>uri.dow))].forEach( dowsString => { let daysFactor = stateData.createDays() - let days = dows.split('').map(Number) - daysFactor.setFromSet(new Set(days)) + daysFactor.setFromSet(new Set(dowsString.split('').map(Number))) } ) } ) } From bbb49aef7c54d9772b65d559a1b36fb1ec0b89b3 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Tue, 16 Jan 2024 17:34:15 +0000 Subject: [PATCH 8/9] restore corridors --- frontend/src/Sidebar/index.jsx | 2 +- frontend/src/Sidebar/restoreStateFromFile.js | 26 +++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/src/Sidebar/index.jsx b/frontend/src/Sidebar/index.jsx index 85d63fc..0674382 100644 --- a/frontend/src/Sidebar/index.jsx +++ b/frontend/src/Sidebar/index.jsx @@ -15,7 +15,7 @@ export default function SidebarContent(){ onDragEnter={ e => { e.stopPropagation(); e.preventDefault() } } onDragOver={ e => { e.stopPropagation(); e.preventDefault() } } onDrop={ event => { - restoreStateFromFile(event,data) + restoreStateFromFile(event,data,logActivity) .then( logActivity('state restored from file') ) // not working? } } > diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index 572d097..fe2b368 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,6 +1,9 @@ +import { Intersection } from '../intersection.js' +import { domain } from '../domain.js' + const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})\/(?true|false)\/(?\d+)/ -export async function restoreStateFromFile(fileDropEvent,stateData){ +export async function restoreStateFromFile(fileDropEvent,stateData,logActivity){ fileDropEvent.stopPropagation() fileDropEvent.preventDefault() @@ -18,12 +21,23 @@ export async function restoreStateFromFile(fileDropEvent,stateData){ distinctPairs(URIs,'startNode','endNode') .forEach( ({startNode,endNode}) => { - console.log('unhandled corridor nodes',startNode,endNode) - /* TODO do something like - but more complicated! let corridor = stateData.createCorridor() - corridor.addIntersection(startNode) - corridor.addIntersection(endNode) - */ + Promise.all( + [startNode,endNode].map(node_id => { + return fetch(`${domain}/node/${node_id}`) + .then( resp => resp.json() ) + .then( node => new Intersection( { + id: node.node_id, + lat: node.geometry.coordinates[1], + lng: node.geometry.coordinates[0], + streetNames: node.street_names + } ) + ) + } ) + ).then( intersections => { + corridor.addIntersection(intersections[0],logActivity) + corridor.addIntersection(intersections[1],logActivity) + }) } ) distinctPairs(URIs,'startTime','endTime') .forEach( ({startTime,endTime}) => { From 5cdc4124a5ac16ba0baa0f6228bb8c827d3faa19 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Wed, 17 Jan 2024 15:22:54 +0000 Subject: [PATCH 9/9] generalize file parsing - now handles any file with URIs --- frontend/src/Sidebar/restoreStateFromFile.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/Sidebar/restoreStateFromFile.js b/frontend/src/Sidebar/restoreStateFromFile.js index fe2b368..bf80744 100644 --- a/frontend/src/Sidebar/restoreStateFromFile.js +++ b/frontend/src/Sidebar/restoreStateFromFile.js @@ -1,7 +1,7 @@ import { Intersection } from '../intersection.js' import { domain } from '../domain.js' -const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})\/(?true|false)\/(?\d+)/ +const URIpattern = /\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d+)\/(?\d{4}-\d{2}-\d{2})\/(?\d{4}-\d{2}-\d{2})\/(?true|false)\/(?\d+)/g export async function restoreStateFromFile(fileDropEvent,stateData,logActivity){ fileDropEvent.stopPropagation() @@ -10,15 +10,10 @@ export async function restoreStateFromFile(fileDropEvent,stateData,logActivity){ // only handle one file at a time let file = fileDropEvent.dataTransfer.files[0] - // TODO: handle CSV - if( file.type != 'application/json' ) { return } - return file.text() - .then( text => JSON.parse(text) ) - .then( data => { + .then( textData => { // should be a list of objects each with a URI property - let URIs = data.map( r => r.URI.match(URIpattern).groups ) - + let URIs = [...textData.matchAll(URIpattern)].map(m=>m.groups) distinctPairs(URIs,'startNode','endNode') .forEach( ({startNode,endNode}) => { let corridor = stateData.createCorridor()