From 775bf9d9b8434c0bd567efe16517666852793ab6 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:44:20 +0300 Subject: [PATCH 01/14] gitignore for unneccessary param files --- src/resources/environment/.gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/resources/environment/.gitignore diff --git a/src/resources/environment/.gitignore b/src/resources/environment/.gitignore new file mode 100644 index 0000000..e69de29 From 3784f7bcdd04c424bff99132055956e6827b7d5a Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:46:57 +0300 Subject: [PATCH 02/14] rewriting project to flask, restructure configuration --- src/resources/environment/dev.params.json | 8 ++++++++ src/resources/global.params.json | 12 ++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/resources/environment/dev.params.json create mode 100644 src/resources/global.params.json diff --git a/src/resources/environment/dev.params.json b/src/resources/environment/dev.params.json new file mode 100644 index 0000000..7ed2c9c --- /dev/null +++ b/src/resources/environment/dev.params.json @@ -0,0 +1,8 @@ +{ + "DATABASE": { + "host": "localhost", + "database": "postgres", + "user": "postgres", + "password": "postgres" + } +} diff --git a/src/resources/global.params.json b/src/resources/global.params.json new file mode 100644 index 0000000..37b7809 --- /dev/null +++ b/src/resources/global.params.json @@ -0,0 +1,12 @@ +{ + "GTFS_DBTABLES": [ + "shapes", + "stop_times", + "trips", + "stops", + "routes", + "calendar", + "agency" + ], + "GTFS_DBSCHEMA" : "gtfs" +} From 00bab8df1301af6f8a390c0bd28296de8118624d Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:47:48 +0300 Subject: [PATCH 03/14] resource loading mechanism --- src/load_resources.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/load_resources.py diff --git a/src/load_resources.py b/src/load_resources.py new file mode 100644 index 0000000..a16a28c --- /dev/null +++ b/src/load_resources.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import os +from dynaconf import Dynaconf + +_path = os.path.dirname(__file__) +APP_ENV = os.environ.get('APP_ENV', 'dev').lower() + +def load_settings(): + # ... and load settings + global_settings_file = os.path.join(_path, 'resources', 'global.params.json') + env_settings_file = os.path.join(_path, 'resources', 'environment', '%s.params.json' % APP_ENV) + override_settings_file = os.path.join(_path, 'resources', 'override', 'params.json') + + # just for logging purposes check which params files seem to be present. + [ + _check_settings_file(f) for f in [ + global_settings_file, + env_settings_file, + override_settings_file, + ] + ] + settings = Dynaconf( + settings_files=[ + global_settings_file, + env_settings_file, + override_settings_file + ] + ) + return settings + +def _check_settings_file(filepath): + if _file_exists_and_has_content(filepath): + #logger.debug('Using settings file from %s' % filepath) + return + #logger.info('Did not find settings file %s or file empty' % filepath) + +def _file_exists_and_has_content(filepath): + return os.path.exists(filepath) and os.path.getsize(filepath) > 0 + +settings = load_settings() From eacdab93b2164fc83a18d38a0f28228921767a35 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:48:34 +0300 Subject: [PATCH 04/14] a lightweight api for getting data --- src/server/__init__.py | 0 src/server/exceptions.py | 15 ++++++++++ src/server/gtfs.py | 54 ++++++++++++++++++++++++++++++++++++ src/server/server.py | 59 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/server/__init__.py create mode 100644 src/server/exceptions.py create mode 100644 src/server/gtfs.py create mode 100644 src/server/server.py diff --git a/src/server/__init__.py b/src/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/server/exceptions.py b/src/server/exceptions.py new file mode 100644 index 0000000..ebef990 --- /dev/null +++ b/src/server/exceptions.py @@ -0,0 +1,15 @@ + +class ToHTTPError(Exception): + status_code = 500 + + def __init__(self, message, status_code=None, payload=None): + Exception.__init__(self) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = dict(self.payload or ()) + rv['message'] = self.message + return rv diff --git a/src/server/gtfs.py b/src/server/gtfs.py new file mode 100644 index 0000000..2263096 --- /dev/null +++ b/src/server/gtfs.py @@ -0,0 +1,54 @@ +import psycopg2 +import json + +from app.server.exceptions import ToHTTPError +from app.load_resources import settings + +SQL_TEMPLATE = """select row_to_json(f.*)::jsonb from (select jsonb_agg(st_asgeojson(z.*)::jsonb)::jsonb as "features", 'FeatureCollection' as "type" from %s z) f""" +SQL_GET_LOCS = """gtfs.loctable_v2""" +SQL_GET_TRIPS = """(select t.*, s.shape as geom from gtfs.trips t, gtfs.calcshapes s where exists (select 1 from gtfs.loctable_v2 l where l.trip_id = t.trip_id) and s.shape_id = t.shape_id)""" + + +class AbstractTableRequestHandler(object): + DATABASE_CONNECTION=None + def __init__(self): + self.partial_sql = None + + def get_data(self): + if not self.DATABASE_CONNECTION or self.DATABASE_CONNECTION.closed == 1: + try: + self.DATABASE_CONNECTION = psycopg2.connect(**settings.DATABASE) + except (Exception, psycopg2.Error) as error: + raise ToHTTPError( + message=f"cannot connect to database", + status_code=500 + ) + with self.DATABASE_CONNECTION.cursor() as cur: + sql = SQL_TEMPLATE % self.partial_sql + cur.execute(sql) + if not cur: + raise ToHTTPError( + message=f"SQL query failed: {sql}", + status_code=404 + ) + return cur.fetchone()[0] + + def serve_request(self): + try: + return json.dumps(self.get_data()) + except ToHTTPError: + raise + except Exception as e: + raise ToHTTPError( + message=str(e), + status_code=500 + ) + +class LocTableRequestHandler(AbstractTableRequestHandler): + def __init__(self): + self.partial_sql = SQL_GET_LOCS + + +class TripTableRequestHandler(AbstractTableRequestHandler): + def __init__(self): + self.partial_sql = SQL_GET_TRIPS diff --git a/src/server/server.py b/src/server/server.py new file mode 100644 index 0000000..1b283da --- /dev/null +++ b/src/server/server.py @@ -0,0 +1,59 @@ +import json +import os + +from flask import Flask, request, send_file, make_response +from flask import Response +from flask import jsonify +from flask_compress import Compress + +from app.server.gtfs import LocTableRequestHandler +from app.server.gtfs import TripTableRequestHandler +from app.server.exceptions import ToHTTPError + +app = Flask(__name__) +Compress(app) + +app.config['COMPRESS_MIMETYPES'].append('application/json') + +@app.errorhandler(ToHTTPError) +def handle_tohttperror(error): + response = jsonify(error.to_dict()) + response.status_code = error.status_code + response.headers = {'Access-Control-Allow-Origin':'*'} + return response + +@app.route("/") +def root(): + return Response( + json.dumps({"message":"Nobody expects the Spanish inquisition!"}), + mimetype='application/json', + headers={ + 'Access-Control-Allow-Origin':'*', + 'Content-Encoding':'UTF-8' + } + ) + +@app.route('/current/locations/') +def loc_table_request(): + return Response( + LocTableRequestHandler().serve_request(), + mimetype='application/json', + headers={ + 'Access-Control-Allow-Origin':'*', + 'Content-Encoding':'UTF-8' + } + ) + +@app.route('/current/trips/') +def trip_table_request(): + return Response( + TripTableRequestHandler().serve_request(), + mimetype='application/json', + headers={ + 'Access-Control-Allow-Origin':'*', + 'Content-Encoding':'UTF-8' + } + ) + +if __name__ == '__main__': + app.run() From 3ea69f90859cd5018887f3cd73eb3f4f9b0ff826 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:49:34 +0300 Subject: [PATCH 05/14] dockerizing --- .gitignore | 1 + build.sh | 15 +++++++++++++++ docker/Dockerfile | 12 ++++++++++++ requirements.txt | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 build.sh create mode 100644 docker/Dockerfile create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 21a0af6..8f66f74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc staticfiles +eoy-build diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e3c17e8 --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash +p_CURDIR=${PWD} + +rm -rf ./eoy-build || true +mkdir ./eoy-build + +cp -r ./src ./eoy-build/app +cp -r ./requirements.txt ./eoy-build/requirements.txt +cp -r ./docker/** ./eoy-build + +cd eoy-build + +docker build --tag localhost/eoy -f Dockerfile . + +cd $p_CURDIR diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..85d5d9e --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3 + +WORKDIR /main + +ENV PYTHONPATH=/main + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD [ "python", "/main/app/server/server.py" ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..74d242a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +Brotli==1.0.9 +certifi==2021.10.8 +charset-normalizer==2.0.12 +click==8.1.3 +dynaconf==3.1.8 +Flask==2.1.2 +Flask-Compress==1.12 +flup6==1.1.1 +idna==3.3 +importlib-metadata==4.11.3 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +psycopg2==2.9.3 +requests==2.27.1 +urllib3==1.26.9 +Werkzeug==2.1.2 +zipp==3.8.0 From 3b518d869dc693a65af01326bd3acdfe08285c5b Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:54:36 +0300 Subject: [PATCH 06/14] update README on running in docker --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f4dcff1..92c51dd 100644 --- a/README.md +++ b/README.md @@ -41,26 +41,53 @@ last function goes to [rcoup](http://gis.stackexchange.com/users/564/rcoup)'s this function will not be necessary anymore and `st_split(geometry, geometry)` can be used instead. +**NOTE:** Tested also on PostgreSQL 14 / PostGIS 3.2 and seems to be running +fine (@tkardi, 18.05.2022) + **NB! Before running the sql file, please read carefully what it does. A sane mind should not run whatever things in a database ;)** Once the database tables and functions have been set up, data can be inserted. ## web API -But still, before data can be loaded to the database, Django (2.2 is the -current LTS version), Django Rest Framework ja Django Rest Framework GIS -should be installed. We need Django for data loading as we'll use Django's -db connection factory. - -You can simply `pip` them +Is based on Flask (Used to be Django, but not any more). + +### Configuration +Configuration is loaded in the following order: +- [resources/global.params.json](/src/resources/global.params.json): this should +contain all app specific settings, regardless of the env we're running in. +- [resources/environment/${APP_ENV}.params.json](/src/resources/dev.params.json): +should contain all environment specific configuration (like db connection params). +The file will selected based on the available `APP_ENV` environment variable +value (case does not matter), and will default to `DEV` if not set. So if you +call your environment `THIS-IS-IT`, then be sure to have a file called +`resources/environment/this-is-it.params.json` present aswell. +- Override parameters should be mounted to `/main/app/resources/override` path. +Expected filename is `params.json`. + +Missing any of these files will not raise an exception during configuration +loading but may hurt afterwards when a specific value that is needed is not +found. + +### Using Docker engine for web API +The Flask app maybe run manually in terminal but the least-dependency-hell-way +seems to be via docker (official latest python:3 image). In the project root +(assuming your database connection is correctly configured in +[resources/environment/dev.params.json](/src/resources/dev.params.json)): ``` -$ pip install django==2.2 -$ pip install djangorestframework -$ pip install pip install djangorestframework-gis +$ source build.sh + [..] +Successfully tagged localhost/eoy:latest +$ docker run -it --rm --network=host -e APP_ENV=DEV --name eoy localhost/eoy:latest +* Serving Flask app 'server' (lazy loading) +* Environment: production + WARNING: This is a development server. Do not use it in a production deployment. + Use a production WSGI server instead. +* Debug mode: off +* Running on http://127.0.0.1:5000 (Press CTRL+C to quit) + [..] ``` -or simply use the [`requirements.txt`](api/requirements.txt) because there are -some other things required aswell. ## Loading data The configuration that is necessary for loading the data is described in @@ -75,7 +102,7 @@ development server with `$ python manage.py runserver` -Point your browser to http://127.0.0.1:8000?format=json and you should see a +Point your browser to http://127.0.0.1:5000/ and you should see a response: `{"message":"Nobody expects the spanish inquisition!"}` @@ -84,12 +111,12 @@ response: HTTP GET queries ### Current locations -http://127.0.0.1:8000/current/locations?format=json +http://127.0.0.1:5000/current/locations/ Returns currently active vehicles and their locations together with data on previous and next stops, and routes. ### Current trips -http://127.0.0.1:8000/current/trips?format=json +http://127.0.0.1:5000/current/trips/ Returns currently active trips as linestrings from the first stop of the trip to the last. From a3bb74c126ae9b361091b0afde1f45af672d274e Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:55:12 +0300 Subject: [PATCH 07/14] update doc + example to new urls --- doc/foss4ge2017_kardi.html | 6 +++--- example/current.html | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/foss4ge2017_kardi.html b/doc/foss4ge2017_kardi.html index f5581d3..76eb5ee 100644 --- a/doc/foss4ge2017_kardi.html +++ b/doc/foss4ge2017_kardi.html @@ -171,11 +171,11 @@

Estimating public transit "real-time" locations based on time-table data

Current locations API
- (Django + Django REST framework) + was: Django + Django REST framework | is currently: Flask
- API
- https://tkardi.ee/current/locations/?format=json + API
+ https://tkardi.ee/gtfs/current/locations/?format=json
Example dashboard
diff --git a/example/current.html b/example/current.html index 327fb92..f81af48 100644 --- a/example/current.html +++ b/example/current.html @@ -73,29 +73,31 @@ "#8BB4C5": "Ferry" }, // vehicle locations layer - realtime = L.realtime('https://tkardi.ee/current/locations/?format=json', { + realtime = L.realtime('https://tkardi.ee/gtfs/current/locations/?format=json', { interval: 3 * 1000, getFeatureId: function(feature) { - return feature.id; + return feature.properties.trip_id; }, pointToLayer: function(feature, latlng) { + console.log(feature); var marker = L.marker(latlng, { icon: L.AwesomeMarkers.icon({ prefix: 'fa', icon: 'fa-bus', markerColor: 'black', - iconColor: feature.properties.route_color + iconColor: feature.properties.route_color.toLowerCase() }), riseOnHover: true }).bindTooltip( L.Util.template( - '{route_short_name}: {trip_long_name}.
Headsign: {trip_headsign}.
Next: {nextstop_name} @ {nextstop_arrive}', + '{route_short_name}: {trip_long_name}.
Headsign: {trip_headsign}.
Next: {next_stop} @ {next_stop_time}', feature.properties ) ); return marker; }, onEachFeature: function(feature, layer) { + //console.log(feature); layer.on({ click: function (e) { follow_layer_id = e.target.feature.id; @@ -106,7 +108,7 @@ feature = layer.feature; layer.setTooltipContent( L.Util.template( - '{route_short_name}: {trip_long_name}.
Headsign: {trip_headsign}.
Next: {nextstop_name} @ {nextstop_arrive}', + '{route_short_name}: {trip_long_name}.
Headsign: {trip_headsign}.
Next: {next_stop} @ {next_stop_time}', feature.properties ) ); From f929b547afc6454ed368830f41acaf89ffd614e4 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 21:57:10 +0300 Subject: [PATCH 08/14] fix issues with db creation + time calculation sql --- db/init.sql | 57 +++++++++++++++++++++++++++++++++++------------ db/preprocess.sql | 2 -- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/db/init.sql b/db/init.sql index ff37900..6e3e204 100644 --- a/db/init.sql +++ b/db/init.sql @@ -1,9 +1,10 @@ -create schema gtfs authorization postgres; +create schema if not exists gtfs authorization postgres; comment on schema gtfs is 'Schema for GTFS data'; /* Tables */ -create table if not exists gtfs.agency ( +drop table if exists gtfs.agency; +create table gtfs.agency ( agency_id integer, agency_name character varying(250), agency_url character varying(250), @@ -17,7 +18,8 @@ alter table gtfs.agency add constraint pk__agency primary key (agency_id); -create table if not exists gtfs.calendar( +drop table if exists gtfs.calendar; +create table gtfs.calendar( service_id integer, monday boolean, tuesday boolean, @@ -34,7 +36,8 @@ alter table gtfs.calendar add constraint pk__calendar primary key (service_id); -create table if not exists gtfs.routes( +drop table if exists gtfs.routes; +create table gtfs.routes( route_id character varying(32), agency_id integer, route_short_name character varying(100), @@ -52,7 +55,8 @@ alter table gtfs.routes add constraint -- deferrable initially deferred; -create table if not exists gtfs.shapes( +drop table if exists gtfs.shapes; +create table gtfs.shapes( shape_id integer, shape_pt_lat numeric, shape_pt_lon numeric, @@ -61,7 +65,8 @@ create table if not exists gtfs.shapes( alter table gtfs.shapes owner to postgres; create unique index uidx__shapes on gtfs.shapes (shape_id, shape_pt_sequence); -create table if not exists gtfs.stops ( +drop table if exists gtfs.stops; +create table gtfs.stops ( stop_id integer, stop_code character varying(100), stop_name character varying(250), @@ -79,8 +84,8 @@ alter table gtfs.stops owner to postgres; alter table gtfs.stops add constraint pk__stops primary key (stop_id); - -create table if not exists gtfs.trips ( +drop table if exists gtfs.trips; +create table gtfs.trips ( route_id character varying(32), service_id integer, trip_id integer, @@ -93,7 +98,8 @@ create table if not exists gtfs.trips ( alter table gtfs.trips owner to postgres; -create table if not exists gtfs.stop_times( +drop table if exists gtfs.stop_times; +create table gtfs.stop_times( trip_id integer, arrival_time character varying(8), departure_time character varying(8), @@ -120,11 +126,16 @@ declare a_cur int; a_strt int; a_fin int; + strt time; + fin interval; totalsecs numeric := 1; fractionsecs numeric := 0; begin - a_fin := extract(epoch from trip_fin::interval); + --a_fin := string_to_array(trip_fin, ':'); + a_fin := extract(epoch from trip_fin::interval); + --a_strt := string_to_array(trip_start, ':'); a_strt := extract(epoch from trip_start::interval); + --a_cur := string_to_array(curtime, ':'); a_cur := extract(epoch from curtime::interval); if a_cur < a_strt then a_cur := a_cur + 24*60*60; @@ -133,9 +144,27 @@ begin fractionsecs := (a_cur::numeric-a_strt::numeric); totalsecs := (a_fin::numeric-a_strt::numeric); end if; --- raise notice 'Fraction %', fractionsecs; --- raise notice 'Total %', totalsecs; -return fractionsecs::numeric / totalsecs::numeric; + +/* if a_fin[1]::smallint >= 24 then + fin := ((a_fin[1]::smallint - 24)::varchar||':'||(a_fin)[2]||':'||(a_fin)[3])::time; + totalsecs := extract(epoch from (('24:00:00'::time - trip_start::time) + fin)); + else + totalsecs := extract(epoch from trip_fin::time - trip_start::time); + end if; + + if a_cur[1]::smallint < a_strt[1]::smallint then + fin := '24:00:00'::time - trip_start::time; + fractionsecs := extract(epoch from (curtime::time + fin)); + else + fractionsecs := extract(epoch from curtime::time - trip_start::time); + end if; + */ + --raise notice 'a_cur %', a_cur; + --raise notice 'a_strt %', a_strt; + --raise notice 'a_fin %', a_fin; + --raise notice 'Fraction %', fractionsecs; + --raise notice 'Total %', totalsecs; + return fractionsecs::numeric / totalsecs::numeric; end; $$ language plpgsql @@ -249,7 +278,7 @@ begin -- doing full speed ->> return whatever timespan we need to cover X := nxt - prv; M := acctime + stoptime; - dt := ((X - 2 * acctime)::numeric / (X - 2 * M)::numeric) * (cur - prv - M)::numeric; + dt := ((X - 2 * acctime)::numeric / (X - 2 * M)::numeric + 0.000001) * (cur - prv - M)::numeric; dt := prv + acctime + dt; end if; return (timestamp 'epoch' + dt * interval '1 second')::time::varchar; diff --git a/db/preprocess.sql b/db/preprocess.sql index 3887447..9659533 100644 --- a/db/preprocess.sql +++ b/db/preprocess.sql @@ -230,7 +230,6 @@ select /* @tkardi 09.11.2021 st_flipcoordinates to quickly get API geojson coors order correct. FIXME: should be a django version gdal version thing. */ - st_flipcoordinates( st_lineinterpolatepoint( trip.shape, gtfs.get_time_fraction( @@ -242,7 +241,6 @@ select trip.cur::character varying ) ) - ) ) as pos from curtime, trip left join gtfs.stops tostop on trip.to_stop_id = tostop.stop_id From 861cf1e89b07a0d69657c502225612258f4cd6fb Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 22:01:47 +0300 Subject: [PATCH 09/14] fix url-typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92c51dd..c2bbf2a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Is based on Flask (Used to be Django, but not any more). Configuration is loaded in the following order: - [resources/global.params.json](/src/resources/global.params.json): this should contain all app specific settings, regardless of the env we're running in. -- [resources/environment/${APP_ENV}.params.json](/src/resources/dev.params.json): +- [resources/environment/${APP_ENV}.params.json](/src/resources/environment/dev.params.json): should contain all environment specific configuration (like db connection params). The file will selected based on the available `APP_ENV` environment variable value (case does not matter), and will default to `DEV` if not set. So if you From e2105297caca824439f15d182de975bd32257e71 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 23:10:52 +0300 Subject: [PATCH 10/14] zip download url to global.params.json --- src/resources/global.params.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resources/global.params.json b/src/resources/global.params.json index 37b7809..7b50138 100644 --- a/src/resources/global.params.json +++ b/src/resources/global.params.json @@ -8,5 +8,6 @@ "calendar", "agency" ], - "GTFS_DBSCHEMA" : "gtfs" + "GTFS_DBSCHEMA" : "gtfs", + "GTFS_ZIPURL" : "http://www.peatus.ee/gtfs/gtfs.zip" } From 3d912094b67115b201adf32f0aebd0ed4e59b696 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 23:11:53 +0300 Subject: [PATCH 11/14] db init+preprocessing to resources and include dataprocessing --- src/resources/db/init.sql | 374 +++++++++++++++++++++++++++ src/resources/db/preprocess.sql | 239 +++++++++++++++++ src/resources/db/scrap/kiirendus.png | Bin 0 -> 65411 bytes src/resources/db/scrap/qtests.sql | 101 ++++++++ src/tools/__init__.py | 0 src/tools/datasync.py | 180 +++++++++++++ 6 files changed, 894 insertions(+) create mode 100644 src/resources/db/init.sql create mode 100644 src/resources/db/preprocess.sql create mode 100644 src/resources/db/scrap/kiirendus.png create mode 100644 src/resources/db/scrap/qtests.sql create mode 100644 src/tools/__init__.py create mode 100644 src/tools/datasync.py diff --git a/src/resources/db/init.sql b/src/resources/db/init.sql new file mode 100644 index 0000000..cfa2626 --- /dev/null +++ b/src/resources/db/init.sql @@ -0,0 +1,374 @@ +create schema if not exists gtfs authorization postgres; +comment on schema gtfs is 'Schema for GTFS data'; + +/* Tables */ + +drop table if exists gtfs.agency; +create table gtfs.agency ( + agency_id integer, + agency_name character varying(250), + agency_url character varying(250), + agency_timezone character varying(100), + agency_phone character varying(100), + agency_lang character varying(3) +); + +alter table gtfs.agency add constraint + pk__agency primary key (agency_id); + + +drop table if exists gtfs.calendar; +create table gtfs.calendar( + service_id integer, + monday boolean, + tuesday boolean, + wednesday boolean, + thursday boolean, + friday boolean, + saturday boolean, + sunday boolean, + start_date date, + end_date date +); +alter table gtfs.calendar add constraint + pk__calendar primary key (service_id); + + +drop table if exists gtfs.routes; +create table gtfs.routes( + route_id character varying(32), + agency_id integer, + route_short_name character varying(100), + route_long_name character varying(250), + route_type smallint, + route_color character varying(10), + competent_authority character varying(100) +); +alter table gtfs.routes add constraint + pk__routes primary key (route_id); + + +drop table if exists gtfs.shapes; +create table gtfs.shapes( + shape_id integer, + shape_pt_lat numeric, + shape_pt_lon numeric, + shape_pt_sequence smallint +); +create unique index uidx__shapes on gtfs.shapes (shape_id, shape_pt_sequence); + +drop table if exists gtfs.stops; +create table gtfs.stops ( + stop_id integer, + stop_code character varying(100), + stop_name character varying(250), + stop_lat numeric, + stop_lon numeric, + zone_id integer, + alias character varying(250), + stop_area character varying(250), + stop_desc character varying(250), + lest_x numeric, + lest_y numeric, + zone_name character varying(250) +); +alter table gtfs.stops add constraint + pk__stops primary key (stop_id); + +drop table if exists gtfs.trips; +create table gtfs.trips ( + route_id character varying(32), + service_id integer, + trip_id integer, + trip_headsign character varying(250), + trip_long_name character varying(250), + direction_code character varying(10), + shape_id integer, + wheelchair_accessible boolean +); + +drop table if exists gtfs.stop_times; +create table gtfs.stop_times( + trip_id integer, + arrival_time character varying(8), + departure_time character varying(8), + stop_id integer, + stop_sequence smallint, + pickup_type smallint, + drop_off_type smallint +); + +/* Functions */ + + +create or replace function gtfs.get_time_fraction( + trip_start varchar, trip_fin varchar, curtime varchar) +returns numeric as +$$ +declare + a_cur int; + a_strt int; + a_fin int; + strt time; + fin interval; + totalsecs numeric := 1; + fractionsecs numeric := 0; +begin + --a_fin := string_to_array(trip_fin, ':'); + a_fin := extract(epoch from trip_fin::interval); + --a_strt := string_to_array(trip_start, ':'); + a_strt := extract(epoch from trip_start::interval); + --a_cur := string_to_array(curtime, ':'); + a_cur := extract(epoch from curtime::interval); + if a_cur < a_strt then + a_cur := a_cur + 24*60*60; + end if; + if a_cur between a_strt and a_fin then + fractionsecs := (a_cur::numeric-a_strt::numeric); + totalsecs := (a_fin::numeric-a_strt::numeric); + end if; + +/* if a_fin[1]::smallint >= 24 then + fin := ((a_fin[1]::smallint - 24)::varchar||':'||(a_fin)[2]||':'||(a_fin)[3])::time; + totalsecs := extract(epoch from (('24:00:00'::time - trip_start::time) + fin)); + else + totalsecs := extract(epoch from trip_fin::time - trip_start::time); + end if; + + if a_cur[1]::smallint < a_strt[1]::smallint then + fin := '24:00:00'::time - trip_start::time; + fractionsecs := extract(epoch from (curtime::time + fin)); + else + fractionsecs := extract(epoch from curtime::time - trip_start::time); + end if; + */ + --raise notice 'a_cur %', a_cur; + --raise notice 'a_strt %', a_strt; + --raise notice 'a_fin %', a_fin; + --raise notice 'Fraction %', fractionsecs; + --raise notice 'Total %', totalsecs; + return fractionsecs::numeric / totalsecs::numeric; +end; +$$ +language plpgsql +security invoker; +comment on function gtfs.get_time_fraction(varchar, varchar, varchar) is 'Calculates the relative fraction that current time represents in between start and finish timestamps.'; + + +/* +------------------------------------------------------------------ +-- TESTS fro gtfs.get_time_fraction (varchar, varchar, varchar) -- +------------------------------------------------------------------ + + +select f.*, gtfs.get_time_fraction (f.str, f.fin, f.cur) as fraction, gtfs.get_time_fraction (f.str, f.fin, f.cur) = f.expected as test +from ( +select '23:00:00' as str, '24:00:00' as fin, '23:01:00' as cur, 1::numeric/60::numeric as expected +union all +select '23:00:00', '24:00:00', '23:30:00', 0.5 +union all +select '23:00:00', '25:00:00', '00:30:00', 0.75 +union all +select '23:00:00', '23:01:00', '23:01:00', 1.0 +union all +select '23:00:00', '23:01:00', '23:00:00', 0.0 +union all +select '00:10:00', '00:44:00', '00:27:00', 0.5 +union all +select '24:10:00', '24:44:00', '00:27:00', 0.5 +union all +select '22:00:00', '28:00:00', '01:00:00', 0.5 +) f; +*/ + + +create or replace function gtfs.get_current_impeded_time( + laststop varchar, nextstop varchar, curtime varchar, + stoptime integer default 10, acctime integer default 10) +returns varchar as +$$ +declare + a_cur varchar[]; + a_prev varchar[]; + a_next varchar[]; + prv numeric; + nxt numeric; + cur numeric; + dt numeric; + X numeric; + M numeric; + nxt_nextday boolean; + prv_nextday boolean; +begin + a_cur := string_to_array(curtime, ':'); + a_prev := string_to_array(laststop, ':'); + a_next := string_to_array(nextstop, ':'); + + if a_cur[1]::smallint < a_prev[1]::smallint then + -- means curtime is past midnight + cur := extract(epoch from '24:00:00'::time) + extract(epoch from curtime::time); + else + -- we are still in the same day as previous stop was + cur := extract(epoch from curtime::time); + end if; + + if a_prev[1]::smallint >= 24 then + -- previous stop was in fact tomorrow + prv := extract(epoch from '24:00:00'::time) + extract(epoch from ((a_prev[1]::smallint - 24)::varchar||':'||(a_prev)[2]||':'||(a_prev)[3])::time); + prv_nextday := true; + else + -- previous stop was today + prv := extract(epoch from laststop::time); + prv_nextday := false; + end if; + + if a_next[1]::smallint >= 24 then + -- next stop will be tomorrow + nxt := extract(epoch from '24:00:00'::time) + extract(epoch from ((a_next[1]::smallint - 24)::varchar||':'||(a_next)[2]||':'||(a_next)[3])::time); + nxt_nextday := true; + else + -- next stop will be today + nxt := extract(epoch from nextstop::time); + nxt_nextday := false; + end if; + + /* Check whether we are currently: a. stopped, b. speeding up, c. slowing down, d. going full speed */ + if (cur - prv < stoptime) then + -- stop at the prev station ->> return time at previous station as current time, + -- but check whether hours are correct and justify + if prv_nextday = false then + return laststop; + else + return ((a_prev[1]::smallint - 24)::varchar||':'||(a_prev)[2]||':'||(a_prev)[3])::time::varchar; + end if; + elsif (nxt - cur < stoptime) then + -- stop at the next station ->> return time at next station as current time, + -- but check whether hours are correct and justify + if nxt_nextday = false then + return nextstop; + else + return ((a_next[1]::smallint - 24)::varchar||':'||(a_next)[2]||':'||(a_next)[3])::time::varchar; + end if; + elsif ((cur - prv) < (stoptime + acctime)) then + -- gathering speed ->> return time with 1:1 ratio + dt := prv + (tan(radians(45)) * (cur - prv - stoptime)); + elsif ((nxt - cur) < (stoptime + acctime)) then + -- slowing down ->> return time with 1:1 ratio + X := nxt - prv; + dt := nxt - (tan(radians(45)) * (nxt - cur - stoptime)); + else + -- doing full speed ->> return whatever timespan we need to cover + X := nxt - prv; + M := acctime + stoptime; + dt := ((X - 2 * acctime)::numeric / (X - 2 * M)::numeric + 0.000001) * (cur - prv - M)::numeric; + dt := prv + acctime + dt; + end if; + return (timestamp 'epoch' + dt * interval '1 second')::time::varchar; +end; +$$ +language plpgsql +security invoker; +comment on function gtfs.get_current_impeded_time( + varchar, varchar, varchar, integer, integer +) is 'Calculates current "impeded time" based on last and next stoptimes and current real time as described in https://github.com/tkardi/eoy/issues/2'; + + +/* + +------------------------------------------------------------------ +-- TESTS fro gtfs.get_current_impeded_time (varchar, varchar, varchar, integer, integer) -- +------------------------------------------------------------------ + +select t.*, gtfs.get_current_impeded_time(t.strt, t.fin, t.cur, 10, 10), +gtfs.get_current_impeded_time(t.strt, t.fin, t.cur, 10, 10) = t.expected as test +from ( + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'stopped' as state, + ('23:59:'||lpad(generate_series(30, 39)::varchar, 2, '0' ))::varchar as cur, + '23:59:30'::varchar as expected +union all + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'accelerating' as state, + ('23:59:'||lpad(generate_series(40, 49)::varchar, 2, '0' ))::varchar as cur, + ('23:59:'||lpad(generate_series(30, 39)::varchar, 2, '0' ))::varchar as expected +union all + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'fullspeed day 1' as state, + ('23:59:'||lpad(generate_series(50, 59)::varchar, 2, '0' ))::varchar as cur, + ('23:59:'||lpad(generate_series(40, 59, 2)::varchar, 2, '0' ))::varchar as expected +union all + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'fullspeed day 2' as state, + ('00:00:'||lpad(generate_series(0, 9)::varchar, 2, '0' ))::varchar as cur, + ('00:00:'||lpad(generate_series(0, 19, 2)::varchar, 2, '0' ))::varchar as expected +union all + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'stopping' as state, + ('00:00:'||lpad(generate_series(10, 19)::varchar, 2, '0' ))::varchar as cur, + ('00:00:'||lpad(generate_series(20, 29)::varchar, 2, '0' ))::varchar as expected +union all + select + '23:59:30'::varchar as strt, + '24:00:30'::varchar as fin, + 'stopped' as state, + ('00:00:'||lpad(generate_series(20, 29)::varchar, 2, '0' ))::varchar as cur, + '00:00:30'::varchar as expected + +) t; +*/ + +/** FUNCTION split_line_multipoint(geometry, geometry) +* by http://gis.stackexchange.com/users/564/rcoup +* posted @ http://gis.stackexchange.com/a/112317 +*/ + +CREATE OR REPLACE FUNCTION public.split_line_multipoint( + input_geom geometry, + blade geometry) + RETURNS geometry AS +$BODY$ + -- this function is a wrapper around the function ST_Split + -- to allow splitting multilines with multipoints + -- + DECLARE + result geometry; + simple_blade geometry; + blade_geometry_type text := GeometryType(blade); + geom_geometry_type text := GeometryType(input_geom); + BEGIN + IF blade_geometry_type NOT ILIKE 'MULTI%' THEN + RETURN ST_Split(input_geom, blade); + ELSIF blade_geometry_type NOT ILIKE '%POINT' THEN + RAISE NOTICE 'Need a Point/MultiPoint blade'; + RETURN NULL; + END IF; + + IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN + RAISE NOTICE 'Need a LineString/MultiLineString input_geom'; + RETURN NULL; + END IF; + + result := input_geom; + -- Loop on all the points in the blade + FOR simple_blade IN SELECT (ST_Dump(ST_CollectionExtract(blade, 1))).geom + LOOP + -- keep splitting the previous result + result := ST_CollectionExtract(ST_Split(result, simple_blade), 2); + END LOOP; + RETURN result; + END; +$BODY$ + LANGUAGE plpgsql IMMUTABLE + COST 100; +comment on function public.split_line_multipoint(geometry, geometry) is + 'Function by http://gis.stackexchange.com/users/564/rcoup posted @ http://gis.stackexchange.com/a/112317'; diff --git a/src/resources/db/preprocess.sql b/src/resources/db/preprocess.sql new file mode 100644 index 0000000..c67ed1c --- /dev/null +++ b/src/resources/db/preprocess.sql @@ -0,0 +1,239 @@ +/** Some tuning/preprocessing. These will have to be wrapped either +* in a db function (so we can call it from datasync.py) +* or alternatively save as plaintext files and let datasync.py +* read them (it) and then execute in a transaction after data sync +* has taken place. +*/ + + +-- drop previous +drop view if exists gtfs.loctable_v2; +drop table if exists gtfs.calcshapes; +drop table if exists gtfs.calcstopnodes; +drop table if exists gtfs.calctrips; + +-- and create anew + +/** table: gtfs.calcshapes +* +* A table for storing full trip shapes (linestrings) and +* collected nodes, which we'll use afterwards for "snapping" +* stops onto trip shapes. +*/ + + +create table gtfs.calcshapes as +select + shape_id, st_makeline(array_agg(shape)) as shape, + st_collect(shape) as nodes +from ( + select + s.shape_id, st_setsrid(st_makepoint(s.shape_pt_lon, s.shape_pt_lat), 4326) as shape + from + gtfs.shapes s + order by s.shape_id, s.shape_pt_sequence) n +group by shape_id +order by shape_id; + +alter table gtfs.calcshapes + add constraint pk__calcshapes primary key (shape_id); +create index sidx__calcshapes + on gtfs.calcshapes using gist (shape); +create index sidx__calcshapes_nodes + on gtfs.calcshapes using gist (shape); + + +/** table: gtfs:calcstopnodes +* +* Stores closest nodes on trip shapes to respective stops. +*/ + + +create table gtfs.calcstopnodes as +select + shape_id, st_multi(st_collect(stop_node)) as stop_nodes, + trip_id, array_agg(stop_seq) as stop_seq +from ( + select + s.shape_id, + st_closestpoint(s.nodes, st_setsrid( + st_point(stops.stop_lon, stops.stop_lat), 4326)) as stop_node, + t.trip_id, st.stop_sequence as stop_seq + from + gtfs.calcshapes s, + gtfs.stop_times st, + gtfs.trips t, + gtfs.stops stops + where + st.stop_id = stops.stop_id and + st.trip_id = t.trip_id and + s.shape_id = t.shape_id + order by s.shape_id, t.trip_id, st.stop_sequence) m +group by shape_id, trip_id; + +alter table gtfs.calcstopnodes + add constraint pk__calcstopnodes primary key (trip_id); +create index sidx__calcstopnodes + on gtfs.calcstopnodes using gist (stop_nodes); + + +/** table: gtfs.calctrips +* +* Break trip shapes up into shorter linestrings based on stops (i.e +* "trip shape closest nodes to stops"). Call these "trip legs". +* A trip leg starts at gtfs.stop_times.departure_time and ends at +* gtfs.stop_times.arrival_time. +*/ + + +create table gtfs.calctrips as +with + splits as ( + select + cs.shape_id, sn.trip_id, + sn.stop_seq, + (st_dump(split_line_multipoint(cs.shape, sn.stop_nodes))).* + from + gtfs.calcshapes cs, + gtfs.calcstopnodes sn + where + cs.shape_id = sn.shape_id + ), + inbetween as ( + select + splits.shape_id, splits.trip_id, + splits.stop_seq[splits.path[1]] as from_stop, + splits.stop_seq[splits.path[1]+1] as to_stop, + splits.geom as shape + from splits + ), + triptimes as ( + select + trip_id, min(departure_time) as trip_start, + max(arrival_time) as trip_fin + from gtfs.stop_times + group by trip_id + ) +select + inbetween.shape_id, inbetween.trip_id, inbetween.shape, + triptimes.trip_start, triptimes.trip_fin, + inbetween.from_stop as from_stop_seq, + from_stops.stop_id as from_stop_id, + from_stops.departure_time as from_stop_time, + inbetween.to_stop as to_stop_seq, + to_stops.stop_id as to_stop_id, + to_stops.arrival_time as to_stop_time +from + inbetween, + gtfs.stop_times from_stops, + gtfs.stop_times to_stops, + triptimes +where + inbetween.trip_id = from_stops.trip_id and + inbetween.trip_id = to_stops.trip_id and + inbetween.from_stop = from_stops.stop_sequence and + inbetween.to_stop = to_stops.stop_sequence and + triptimes.trip_id = inbetween.trip_id +order by inbetween.trip_id, inbetween.from_stop; + +create index sidx__calctrips + on gtfs.calctrips using gist (shape); + +/** view: gtfs.loctable_v2 +* +* Estimated locations of currently running public transport. This +* is the view that will be queried for current location of public transit +* vehicles. +*/ + + +create or replace view gtfs.loctable_v2 as +with + curtime as ( + select + clock_timestamp()::date AS cd, + to_char(clock_timestamp(), 'hh24:mi:ss'::text) AS ct, + date_part('dow'::text, clock_timestamp()) + 1 AS d, + lpad((to_char(clock_timestamp(), 'hh24')::int + 24)::varchar,2,'0')||':'||to_char(clock_timestamp(), 'mi:ss') as plushours + ), + cal as ( + select + c.service_id + from + gtfs.calendar c, + curtime + where + curtime.cd >= c.start_date and + curtime.cd <= c.end_date and + (array[ + c.monday, + c.tuesday, + c.wednesday, + c.thursday, + c.friday, + c.saturday, + c.sunday])[curtime.d] = true + ), + startstop as ( + select + calctrips.trip_id, calctrips.from_stop_time as leg_start, + calctrips.to_stop_time as leg_fin, calctrips.from_stop_id, + calctrips.to_stop_id, calctrips.from_stop_seq, + calctrips.to_stop_seq, calctrips.shape, + calctrips.trip_start, calctrips.trip_fin + from + gtfs.calctrips, curtime + where + ( + curtime.ct >= calctrips.from_stop_time::text and + curtime.ct <= calctrips.to_stop_time::text + ) or ( + curtime.plushours >= calctrips.from_stop_time::text and + curtime.plushours <= calctrips.to_stop_time::text + ) + ), + trip as ( + select + startstop.trip_id, trips.shape_id, + startstop.trip_start, startstop.trip_fin, + startstop.leg_start, startstop.leg_fin, + curtime.ct as cur, trips.trip_headsign, + trips.trip_long_name, routes.route_short_name, + routes.route_long_name, routes.route_color, + startstop.from_stop_id, startstop.to_stop_id, + startstop.from_stop_seq, startstop.to_stop_seq, + startstop.shape + from + cal, curtime, startstop, gtfs.trips trips, gtfs.routes routes + where + trips.trip_id = startstop.trip_id and + trips.service_id = cal.service_id and + trips.route_id::text = routes.route_id::text + ) +select + trip.trip_id, trip.shape_id, trip.trip_start, trip.trip_fin, + trip.trip_headsign, trip.trip_long_name, trip.route_short_name, + trip.route_long_name, + tostop.stop_id as next_stop_id, + tostop.stop_name as next_stop, + trip.leg_fin as next_stop_time, + fromstop.stop_id as prev_stop_id, + fromstop.stop_name as prev_stop, + trip.leg_start as prev_stop_time, + curtime.ct as current_time, + '#'::text || trip.route_color::text as route_color, + st_lineinterpolatepoint( + trip.shape, + gtfs.get_time_fraction( + trip.leg_start, + trip.leg_fin, + gtfs.get_current_impeded_time( + trip.leg_start, + trip.leg_fin, + trip.cur::character varying + ) + ) + ) as pos +from curtime, trip + left join gtfs.stops tostop on trip.to_stop_id = tostop.stop_id + left join gtfs.stops fromstop on trip.from_stop_id = fromstop.stop_id; diff --git a/src/resources/db/scrap/kiirendus.png b/src/resources/db/scrap/kiirendus.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa65abf6f162df0676e48eaca1216c86e2e6d75 GIT binary patch literal 65411 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

|}esLcDlI+1{JmtMD}Y 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 Y1daY+HWy6KBLTkd>6z))YdgL8e-6{cUH||9 literal 0 HcmV?d00001 diff --git a/src/resources/db/scrap/qtests.sql b/src/resources/db/scrap/qtests.sql new file mode 100644 index 0000000..ccfa432 --- /dev/null +++ b/src/resources/db/scrap/qtests.sql @@ -0,0 +1,101 @@ +with + startstop as ( + select trip_id, min(arrival_time) as trip_start, max(departure_time) as trip_fin + from gtfs.stop_times + group by trip_id + ), + curtime as ( + select clock_timestamp()::date as cd, + to_char(clock_timestamp(), 'hh24:mi:ss') as ct, + extract (dow from clock_timestamp()) as d + ), + cal as ( + select c.service_id + from gtfs.calendar c, curtime + where + curtime.cd between c.start_date and c.end_date and + (array[c.monday, c.tuesday, c.wednesday, c.thursday, c.friday, c.saturday, c.sunday])[extract (dow from cd)] = true + ), + trip as ( + select + startstop.trip_id, + trips.shape_id, + gtfs.get_time_fraction(startstop.trip_start, startstop.trip_fin, curtime.ct) as fraction, + startstop.trip_start as strt, + startstop.trip_fin as fin, + curtime.ct as cur, + trips.trip_headsign, + trips.trip_long_name, + routes.route_short_name, + routes.route_long_name, + routes.route_color + from cal, curtime, startstop, gtfs.trips trips, gtfs.routes routes + where + startstop.trip_start < curtime.ct and + startstop.trip_fin > curtime.ct and + trips.trip_id = startstop.trip_id and + trips.service_id = cal.service_id and + trips.route_id = routes.route_id + ), + nextstop as ( + select + n.trip_id, s.stop_id, s.stop_lon, s.stop_lat, s.stop_name, + st.arrival_time, st.departure_time, st.stop_sequence + from + gtfs.stops s, gtfs.stop_times st, + (select + trip.trip_id, min(st.stop_sequence) as seq + from + gtfs.stop_times st, trip, curtime + where + st.trip_id = trip.trip_id and + st.arrival_time > curtime.ct + group by trip.trip_id) n + where + s.stop_id = st.stop_id and + n.trip_id = st.trip_id and + n.seq = st.stop_sequence + ), + prevstop as ( + select + n.trip_id, s.stop_id, s.stop_lon, s.stop_lat, s.stop_name, + st.arrival_time, st.departure_time, st.stop_sequence + from + gtfs.stops s, gtfs.stop_times st, + (select + trip.trip_id, max(st.stop_sequence) as seq + from + gtfs.stop_times st, trip, curtime + where + st.trip_id = trip.trip_id and + st.arrival_time < curtime.ct + group by trip.trip_id) n + where + s.stop_id = st.stop_id and + n.trip_id = st.trip_id and + n.seq = st.stop_sequence + ), + shp as ( + select shape_id, st_makeline(array_agg(shape)) as shape + from ( + select s.shape_id, st_setsrid(st_makepoint(s.shape_pt_lon, s.shape_pt_lat), 4326) as shape + from gtfs.shapes s, trip t + where t.shape_id = s.shape_id + order by s.shape_id, s.shape_pt_sequence) n + group by shape_id + ) +select + trip.trip_id, trip.shape_id, trip.strt, trip.fin, trip.cur, + nextstop.stop_id, nextstop.stop_lon, nextstop.stop_lat, nextstop.stop_name, + nextstop.arrival_time, nextstop.departure_time, nextstop.stop_sequence, + prevstop.stop_id, prevstop.stop_lon, prevstop.stop_lat, prevstop.stop_name, + prevstop.arrival_time, prevstop.departure_time, prevstop.stop_sequence, + trip.trip_headsign, trip.trip_long_name, + trip.route_short_name, trip.route_long_name, + '#'||trip.route_color as route_color, + st_lineinterpolatepoint(shp.shape, trip.fraction) as pos +from shp, trip, nextstop, prevstop +where + trip.shape_id = shp.shape_id and + nextstop.trip_id = trip.trip_id and + prevstop.trip_id = trip.trip_id; diff --git a/src/tools/__init__.py b/src/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tools/datasync.py b/src/tools/datasync.py new file mode 100644 index 0000000..dd3d849 --- /dev/null +++ b/src/tools/datasync.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +import io +import psycopg2 +import requests +import shutil +import tempfile +import csv #unicodecsv +import time +import zipfile + +from io import StringIO +import sys, os + +from app.load_resources import settings + +_PATH = os.path.dirname(__file__) + +class FilteredCSVFile(csv.DictReader, object): + """Local helper for reading only specified columns from a csv file. + + It's assumed that row number 1 is the header row. + """ + def __init__( + self, csvfile, fieldnames=None, restkey=None, restval=None, + dialect='excel', *args, **kwargs): + self._header = self.get_csv_header(csvfile) + super(FilteredCSVFile, self).__init__( + csvfile, self._header , restkey, restval, + dialect, *args, **kwargs) + self._fieldnames = fieldnames + + def get_csv_header(self, fp): + return fp.readline().strip('\n').split(',') + + def cleanup(self, obj): + if obj == None or obj == "": + obj = '\\N' + if isinstance(obj, str): + obj = obj.replace('\t', ' ') + if ',' in obj: + obj = '"%s"' % obj + return obj + + def next(self): + row = dict(zip(self._header, next(self.reader))) + return '\t'.join(['%s' % self.cleanup(row[k]) for k in self._fieldnames]) + + def readline(self): + return self.next() + + def read(self): + o = io.StringIO() + try: + while True: + row = self.next() + o.write(row) + o.write(u'\n') + except StopIteration as si: + pass + return o.getvalue() + + +def download_zip(url, to_path): + """Download zipfile from url and extract it to to_path. + + Returns the path of extraction. + """ + filename = url.split('/')[-1] + r = requests.get(url) + r.raise_for_status() + content = io.BytesIO(r.content) + with zipfile.ZipFile(content) as z: + z.extractall(to_path) + return to_path + +def get_csv_header(filepath): + """Retuns csv file's header row.""" + with open(filepath) as n: + return n.readline().strip('\n').split(',') + +def _db_check_table(cursor, dbschema, dbtable): + """Checks input table's existance in the database. + + Returns a list with table's column names. + """ + tab = f'{dbschema}.{dbtable}' + sql = "select array_agg(attname) from pg_attribute " \ + "where attrelid=%s::regclass and not attisdropped and attnum > 0" + params = (tab,) + cursor.execute(sql, params) + return cursor.fetchone()[0] + +def _fs_check_csv(path, filename, ext='txt'): + """Checks if the input csv file really exists. + + Returns a tuple of csv absolute filepath, and headers. + """ + filename = f'{filename}.{ext}' + fp = os.path.join(path, filename) + assert os.path.exists(fp) + return fp, get_csv_header(fp) + +def _get_insert_cols(db_cols, fp_cols, dbschema, tablename): + """Returns intersection of input column names. + + Use this to figure out which columns need to be read from the csv file. + """ + cols = list(set(db_cols).intersection(fp_cols)) + assert len(cols) > 0, f"{dbschema}.{dbtable} and {dbtable}.csv do not share any columns" + return cols + +def _db_prepare_truncate(tableschema, tablename): + """Prepare a truncate statement for a table in the database. + + @FIXME: as this is prone to injection check whether the tablename + mentioned in args really exists. + """ + sql = f"""truncate table {tableschema}.{tablename} cascade""" + return sql + +#{main + +def run(): + """Run data download and database sync operations""" + try: + # go get all csv files extracted at to_path. + # local + # to_path = 'tmp' + # the real thing + to_path = download_zip(settings.GTFS_ZIPURL, tempfile.mkdtemp(prefix='eoy_')) + print(to_path) + # loop through required files and look for a matching table + # in the database + # if found truncate it and insert new rows from the csv file + # if table not found, raise exception + # if exception, then rollback and stop whatever was going on + # all database commands run in a single transaction + with psycopg2.connect(**settings.DATABASE) as connection: + with connection.cursor() as cursor: + cursor.execute(f'SET search_path={settings.GTFS_DBSCHEMA},"$user",public') + # loop through the list of tables specified at + # settings.GTFS_DBTABLES + for dbtable in settings.GTFS_DBTABLES: + # check if table exists in db and get it's columns + db_cols = _db_check_table(cursor, settings.GTFS_DBSCHEMA, dbtable) + # check if file present and get csv header + fp, fp_cols = _fs_check_csv(to_path, dbtable) + print (f'{settings.GTFS_DBSCHEMA}.{dbtable}') + # get intersection of db_cols and fp_cols (i.e cols that + # are present in both) + cols = _get_insert_cols(db_cols, fp_cols, settings.GTFS_DBSCHEMA, dbtable) + # truncate old data, + st_trunc = _db_prepare_truncate(settings.GTFS_DBSCHEMA, dbtable) + cursor.execute(st_trunc) + # and fill anew ... + with open(fp, encoding='utf-8') as f: + fcsv = FilteredCSVFile(f, fieldnames=cols, quotechar='"') + #tab = '%s.%s' % (settings.GTFS_DBSCHEMA, dbtable) + cursor.copy_from(io.StringIO(fcsv.read()), dbtable, sep='\t', columns=cols) + print(cursor.rowcount) + print(f'done {fp}') + except: + raise + shutil.rmtree(to_path) + +def postprocess(): + with psycopg2.connect(**settings.DATABASE) as connection: + fp = os.path.join(os.path.dirname(_PATH), 'resources', 'db', 'preprocess.sql') + with open(fp) as f: + statements = f.read() + with connection.cursor() as cursor: + for statement in statements.split(';'): + if statement.strip() != '': + cursor.execute(statement.strip()) + + +if __name__ == '__main__': + run() + postprocess() + #pass From de6b19439c42b0b326756ca8e2d0a222e3bac916 Mon Sep 17 00:00:00 2001 From: tkardi Date: Wed, 18 May 2022 23:23:12 +0300 Subject: [PATCH 12/14] update README.md --- README.md | 46 +++++++++++++++++++++++-------------------- src/tools/datasync.py | 2 ++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c2bbf2a..b9fb697 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ The process of getting this thing up and running is currently a bit tedious, but we'll live with that for now. ## Database -Expects presence of PostgreSQL (9.4+) / PostGIS (2.1+). As a privileged user run -[db/init.sql](db/init.sql). This will create a database schema called `gtfs`, +Expects presence of PostgreSQL (10+) / PostGIS (2.4+). As a privileged user run +[resources/db/init.sql](src/resources/db/init.sql). +This will create a database schema called `gtfs`, a few tables into it (`gtfs.agency`, `gtfs.calendar`, `gtfs.routes`, `gtfs.shapes`, `gtfs.stop_times`, `gtfs.stops`, `gtfs.trips`) and three functions for dealing with location calculation (`gtfs.get_current_impeded_time`, @@ -41,7 +42,7 @@ last function goes to [rcoup](http://gis.stackexchange.com/users/564/rcoup)'s this function will not be necessary anymore and `st_split(geometry, geometry)` can be used instead. -**NOTE:** Tested also on PostgreSQL 14 / PostGIS 3.2 and seems to be running +**NOTE:** Tested on PostgreSQL 14 / PostGIS 3.2 and seems to be running fine (@tkardi, 18.05.2022) **NB! Before running the sql file, please read carefully what it does. A sane @@ -49,10 +50,7 @@ mind should not run whatever things in a database ;)** Once the database tables and functions have been set up, data can be inserted. -## web API -Is based on Flask (Used to be Django, but not any more). - -### Configuration +## Configuration Configuration is loaded in the following order: - [resources/global.params.json](/src/resources/global.params.json): this should contain all app specific settings, regardless of the env we're running in. @@ -69,7 +67,7 @@ Missing any of these files will not raise an exception during configuration loading but may hurt afterwards when a specific value that is needed is not found. -### Using Docker engine for web API +## Docker The Flask app maybe run manually in terminal but the least-dependency-hell-way seems to be via docker (official latest python:3 image). In the project root (assuming your database connection is correctly configured in @@ -79,6 +77,25 @@ seems to be via docker (official latest python:3 image). In the project root $ source build.sh [..] Successfully tagged localhost/eoy:latest +$ +``` + +## Load data +The configuration that is necessary for loading the data is described in +[Configuration](#configuration). To start the loading procedure +you need to run [tools/datasync.py](src/tools/datasync.py) + +``` +$ docker run -it --rm --network=host -e APP_ENV=DEV --name eoy localhost/eoy:latest python /main/app/tools/datasync.py + [..] +postprocess done +$ +``` + +## web API +Is based on Flask (Used to be Django, but not any more). + +``` $ docker run -it --rm --network=host -e APP_ENV=DEV --name eoy localhost/eoy:latest * Serving Flask app 'server' (lazy loading) * Environment: production @@ -89,19 +106,6 @@ $ docker run -it --rm --network=host -e APP_ENV=DEV --name eoy localhost/eoy:lat [..] ``` -## Loading data -The configuration that is necessary for loading the data is described in -[api/conf/settings.py](api/conf/settings.py). To start the loading procedure -you need to run [api/sync/datasync.py](api/sync/datasync.py) - -`$ python datasync.py` - -And after the loading has finished, again, as a privileged user run -[db/preprocess.sql](db/preprocess.sql). Then we can fire up Django's -development server with - -`$ python manage.py runserver` - Point your browser to http://127.0.0.1:5000/ and you should see a response: diff --git a/src/tools/datasync.py b/src/tools/datasync.py index dd3d849..e946212 100644 --- a/src/tools/datasync.py +++ b/src/tools/datasync.py @@ -164,6 +164,7 @@ def run(): shutil.rmtree(to_path) def postprocess(): + print("starting postprocess...") with psycopg2.connect(**settings.DATABASE) as connection: fp = os.path.join(os.path.dirname(_PATH), 'resources', 'db', 'preprocess.sql') with open(fp) as f: @@ -172,6 +173,7 @@ def postprocess(): for statement in statements.split(';'): if statement.strip() != '': cursor.execute(statement.strip()) + print("postprocess done") if __name__ == '__main__': From 9bea0e00b09083f266b4db0df3ab003097fc746a Mon Sep 17 00:00:00 2001 From: tkardi Date: Thu, 19 May 2022 00:03:18 +0300 Subject: [PATCH 13/14] add flightradar to new structure --- src/server/flightradar.py | 33 +++++++++++++++++++++++++++++++++ src/server/server.py | 12 +++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/server/flightradar.py diff --git a/src/server/flightradar.py b/src/server/flightradar.py new file mode 100644 index 0000000..b20725f --- /dev/null +++ b/src/server/flightradar.py @@ -0,0 +1,33 @@ +import requests +from collections import OrderedDict + + +class FlightRadar(object): + def __init__(self, *args, **kwargs): + self.url = 'https://opensky-network.org/api/states/all?lamin=57.48&lomin=21.6&lamax=59.82&lomax=28.52' + self.keys = [ + 'icao24','callsign','origin_country','time_position', + 'last_contact','longitude','latitude','baro_altitude', + 'on_ground','velocity','true_track','vertical_rate','sensors', + 'geo_altitude','squawk','spi','position_source' + ] + + def get_flight_radar_data(self): + r = requests.get(self.url) + r.raise_for_status() + return self._to_geojson(r.json()) + + def _to_geojson(self, data): + f = [ + OrderedDict( + type='Feature', + id=ac[0], + geometry=OrderedDict(type='Point', coordinates=[ac[5],ac[6]]), + properties=OrderedDict(zip(self.keys, ac)) + ) for ac in data.get('states', []) + ] + + return dict( + type='FeatureCollection', + features=f + ) diff --git a/src/server/server.py b/src/server/server.py index 1b283da..63a9a45 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -8,6 +8,7 @@ from app.server.gtfs import LocTableRequestHandler from app.server.gtfs import TripTableRequestHandler +from app.server.flightradar import FlightRadar from app.server.exceptions import ToHTTPError app = Flask(__name__) @@ -28,7 +29,6 @@ def root(): json.dumps({"message":"Nobody expects the Spanish inquisition!"}), mimetype='application/json', headers={ - 'Access-Control-Allow-Origin':'*', 'Content-Encoding':'UTF-8' } ) @@ -39,7 +39,6 @@ def loc_table_request(): LocTableRequestHandler().serve_request(), mimetype='application/json', headers={ - 'Access-Control-Allow-Origin':'*', 'Content-Encoding':'UTF-8' } ) @@ -50,10 +49,17 @@ def trip_table_request(): TripTableRequestHandler().serve_request(), mimetype='application/json', headers={ - 'Access-Control-Allow-Origin':'*', 'Content-Encoding':'UTF-8' } ) +@app.route('/current/flightradar/') +def flightradar_get(): + return Response( + response=json.dumps(FlightRadar().get_flight_radar_data()), + status=200, + mimetype='application/json' + ) + if __name__ == '__main__': app.run() From 596374478cad51d82da14d5bf6ff285497684c32 Mon Sep 17 00:00:00 2001 From: tkardi Date: Thu, 19 May 2022 00:13:02 +0300 Subject: [PATCH 14/14] remove files that have been moved to the new structure --- api/api/__init__.py | 0 api/api/settings.py | 158 ---------- api/api/urls.py | 47 --- api/api/wsgi.py | 16 - api/eoy/__init__.py | 0 api/eoy/flaskapp.py | 26 -- api/eoy/migrations/__init__.py | 0 api/eoy/models.py | 54 ---- api/eoy/proxy.py | 69 ----- api/eoy/serializers.py | 15 - api/eoy/templatetags/__init__.py | 0 api/eoy/templatetags/eoy_extras.py | 1 - api/eoy/tests.py | 3 - api/eoy/views.py | 41 --- api/manage.py | 10 - api/requirements.txt | 27 -- api/sync/__init__.py | 0 api/sync/datasync.py | 190 ------------ api/sync/preprocess-all.sql | 203 ------------- api/templates/list_rows.html | 162 ---------- api/templates/map.html | 173 ----------- api/templatetags/extra_templatetags.py | 29 -- db/init.sql | 395 ------------------------- db/preprocess.sql | 249 ---------------- db/scrap/kiirendus.png | Bin 65411 -> 0 bytes db/scrap/qtests.sql | 101 ------- 26 files changed, 1969 deletions(-) delete mode 100644 api/api/__init__.py delete mode 100644 api/api/settings.py delete mode 100644 api/api/urls.py delete mode 100644 api/api/wsgi.py delete mode 100644 api/eoy/__init__.py delete mode 100644 api/eoy/flaskapp.py delete mode 100644 api/eoy/migrations/__init__.py delete mode 100644 api/eoy/models.py delete mode 100644 api/eoy/proxy.py delete mode 100644 api/eoy/serializers.py delete mode 100644 api/eoy/templatetags/__init__.py delete mode 120000 api/eoy/templatetags/eoy_extras.py delete mode 100644 api/eoy/tests.py delete mode 100644 api/eoy/views.py delete mode 100644 api/manage.py delete mode 100644 api/requirements.txt delete mode 100644 api/sync/__init__.py delete mode 100644 api/sync/datasync.py delete mode 100644 api/sync/preprocess-all.sql delete mode 100644 api/templates/list_rows.html delete mode 100644 api/templates/map.html delete mode 100644 api/templatetags/extra_templatetags.py delete mode 100644 db/init.sql delete mode 100644 db/preprocess.sql delete mode 100644 db/scrap/kiirendus.png delete mode 100644 db/scrap/qtests.sql diff --git a/api/api/__init__.py b/api/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/api/settings.py b/api/api/settings.py deleted file mode 100644 index e273cd5..0000000 --- a/api/api/settings.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Django settings for api project. - -Generated by 'django-admin startproject' using Django 1.8.11. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os -from conf.settings import DEBUG, SECRET_KEY, ALLOWED_HOSTS, DB_USER, DB_PASSWORD -from conf.settings import DB_NAME, SYNC_USER, SYNC_PASSWORD, DB_HOST -from conf.settings import INSTALLED_APPS_X - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# Application definition - -INSTALLED_APPS = ( - #'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.gis', - 'rest_framework', - 'rest_framework_gis', - 'eoy' -) - -INSTALLED_APPS = INSTALLED_APPS + INSTALLED_APPS_X - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) - -ROOT_URLCONF = 'api.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'api.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - -DATABASES = { - 'default': { - 'NAME': DB_NAME, - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'HOST': DB_HOST, - 'USER': DB_USER, - 'PASSWORD': DB_PASSWORD, - 'PORT': '5432' - }, - 'sync': { - 'NAME': DB_NAME, - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'HOST': DB_HOST, - 'USER': SYNC_USER, - 'PASSWORD': SYNC_PASSWORD, - 'PORT': '5432' - } -} - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates'), - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -REST_FRAMEWORK = { - 'DEFAULT_RENDERER_CLASSES': [ - 'rest_framework.renderers.TemplateHTMLRenderer', - 'rest_framework.renderers.JSONRenderer', -# 'rest_framework.renderers.BrowsableAPIRenderer', - ] -} - -#LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'handlers': { -# 'console': { -# 'class': 'logging.StreamHandler', -# }, -# }, -# 'loggers': { -# 'django.db.backends': { -# 'level': 'DEBUG', -# 'handlers': ['console', ], -# }, -# } -#} - - -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ - -LANGUAGE_CODE = 'et-EE' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') diff --git a/api/api/urls.py b/api/api/urls.py deleted file mode 100644 index 2a2e420..0000000 --- a/api/api/urls.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -"""api URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf.urls import url -from django.urls import include, path -from django.contrib import admin -from rest_framework.urlpatterns import format_suffix_patterns -from conf.settings import INSTALLED_APPS_X - -from eoy import views - -urlpatterns = [ - #url(r'^admin/', include(admin.site.urls)), - url(r'^$', views.index, name='home'), - url( - r'^current/locations/$', views.LocTableAsList.as_view(), - name='locations-list'), - url( - r'^current/trips/$', views.index, - name='trips-list'), - url( - r'^current/flightradar/$', views.flightradar, - name='flights-list'), - url( - r'^current/traingps/$', views.traingps, - name='trains-list'), -] - -## add some extra urls -for app in INSTALLED_APPS_X: - urlpatterns.append( - path('', include('%s.urls' % app)), - ) - -urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html']) diff --git a/api/api/wsgi.py b/api/api/wsgi.py deleted file mode 100644 index f0ac3a7..0000000 --- a/api/api/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for api project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") - -application = get_wsgi_application() diff --git a/api/eoy/__init__.py b/api/eoy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/eoy/flaskapp.py b/api/eoy/flaskapp.py deleted file mode 100644 index d2de739..0000000 --- a/api/eoy/flaskapp.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -from flask import Flask -from flask import Response - -from proxy import flightradar -from proxy import traingps - - - -app = Flask(__name__) - -@app.route('/flightradar') -def flightradar_get(): - return Response( - response=json.dumps(flightradar.get_flight_radar_data()), - status=200, - mimetype='application/json' - ) - -@app.route('/traingps') -def traingps_get(): - return Response( - response=json.dumps(traingps.get_train_gps_data()), - status=200, - mimetype='application/json' - ) diff --git a/api/eoy/migrations/__init__.py b/api/eoy/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/eoy/models.py b/api/eoy/models.py deleted file mode 100644 index 0d96d10..0000000 --- a/api/eoy/models.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -from django.contrib.gis.db import models - -class Stops(models.Model): - stop_id = models.IntegerField(primary_key=True) - stop_name = models.CharField(max_length=250) - stop_lat = models.DecimalField(max_digits=12, decimal_places=9) - stop_lon = models.DecimalField(max_digits=12, decimal_places=9) - - class Meta: - managed = False - db_table = 'gtfs\".\"stops' - -class CurrentLocations(models.Model): - trip_id = models.IntegerField( - primary_key=True) - shape_id = models.IntegerField() - trip_start_time = models.CharField( - max_length=8, db_column='trip_start') - trip_end_time = models.CharField( - max_length=8, db_column='trip_fin') - current_time = models.CharField( - max_length=8, db_column='current_time') - #prevstop_id = models.ForeignKey( - # 'Stops', related_name='prevstop', db_column='prev_stop_id') - prevstop_name = models.CharField( - max_length=250, db_column='prev_stop') - prevstop_depart = models.CharField( - max_length=8, db_column='prev_stop_time') - #prevstop_seq = models.IntegerField() - #nextstop_id = models.ForeignKey( - # 'Stops', related_name='nextstop', db_column='next_stop_id') - nextstop_name = models.CharField( - max_length=250, db_column='next_stop') - nextstop_arrive = models.CharField( - max_length=8, db_column='next_stop_time') - #nextstop_seq = models.IntegerField() - trip_headsign = models.CharField(max_length=250) - trip_long_name = models.CharField(max_length=250) - route_short_name = models.CharField(max_length=100) - route_long_name = models.CharField(max_length=255) - route_color = models.CharField(max_length=10) - location = models.PointField(srid=4326, db_column='pos') - - class Meta: - managed = False - db_table = 'gtfs\".\"loctable_v2' - - -#class CurrentTrips(models.Model): -# -# class Meta: -# managed = False -# db_table = 'gtfs\".\"calctrips' diff --git a/api/eoy/proxy.py b/api/eoy/proxy.py deleted file mode 100644 index 3ca20f0..0000000 --- a/api/eoy/proxy.py +++ /dev/null @@ -1,69 +0,0 @@ -import requests -from collections import OrderedDict - - -class FlightRadar(object): - def __init__(self, *args, **kwargs): - self.url = 'https://opensky-network.org/api/states/all?lamin=57.48&lomin=21.6&lamax=59.82&lomax=28.52' - self.keys = [ - 'icao24','callsign','origin_country','time_position', - 'last_contact','longitude','latitude','baro_altitude', - 'on_ground','velocity','true_track','vertical_rate','sensors', - 'geo_altitude','squawk','spi','position_source' - ] - - def get_flight_radar_data(self): - r = requests.get(self.url) - r.raise_for_status() - return self._to_geojson(r.json()) - - def _to_geojson(self, data): - f = [ - OrderedDict( - type='Feature', - id=ac[0], - geometry=OrderedDict(type='Point', coordinates=[ac[5],ac[6]]), - properties=OrderedDict(zip(self.keys, ac)) - ) for ac in data.get('states', []) - ] - - return dict( - type='FeatureCollection', - features=f - ) - -class TrainGPS(object): - def __init__(self, *args, **kwargs): - self.url = 'http://elron.ee/api/v1/map' - - def _to_float(self, string): - try: - return float(string) - except: - return string - - def get_train_gps_data(self): - r = requests.get(self.url) - r.raise_for_status() - return self._to_geojson(r.json()) - - def _to_geojson(self, data): - f = [ - OrderedDict( - type='Feature', - id=ac['reis'], - geometry=OrderedDict(type='Point', coordinates=[ - self._to_float(ac.get('longitude', "0")), - self._to_float(ac.get('latitude', "0"))] - ), - properties=OrderedDict(ac.copy()) - ) for ac in data.get('data', []) - ] - - return dict( - type='FeatureCollection', - features=f - ) - -flightradar = FlightRadar() -traingps = TrainGPS() diff --git a/api/eoy/serializers.py b/api/eoy/serializers.py deleted file mode 100644 index 82ed06f..0000000 --- a/api/eoy/serializers.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework_gis.serializers import GeoFeatureModelSerializer - -from eoy.models import CurrentLocations - -class LocationTableSerializer(GeoFeatureModelSerializer): - class Meta: - fields = ['trip_id', 'shape_id', 'trip_start_time', - 'trip_end_time', 'current_time', - 'prevstop_name', 'prevstop_depart', - 'nextstop_name', 'nextstop_arrive', 'trip_headsign', - 'trip_long_name', 'route_short_name', 'route_long_name', - 'route_color'] - model = CurrentLocations - lookup_field = 'trip_id' - geo_field = 'location' diff --git a/api/eoy/templatetags/__init__.py b/api/eoy/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/eoy/templatetags/eoy_extras.py b/api/eoy/templatetags/eoy_extras.py deleted file mode 120000 index 7607d7a..0000000 --- a/api/eoy/templatetags/eoy_extras.py +++ /dev/null @@ -1 +0,0 @@ -../../templatetags/extra_templatetags.py \ No newline at end of file diff --git a/api/eoy/tests.py b/api/eoy/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/api/eoy/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/api/eoy/views.py b/api/eoy/views.py deleted file mode 100644 index 9fe3a1a..0000000 --- a/api/eoy/views.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from rest_framework import generics -from rest_framework.decorators import api_view -from rest_framework.response import Response - -from eoy.models import CurrentLocations -from eoy import serializers - -from eoy.proxy import flightradar as f -from eoy.proxy import traingps as t - -# Create your views here. -@api_view(('GET', )) -def index(request, *args, **kwargs): - """Yes-yes-yes,... I'm up and running.""" - return Response({"message":"Nobody expects the spanish inquisition!"}) - -class LocTableAsList(generics.ListAPIView): - model = CurrentLocations - queryset = model.objects.all() - serializer_class = serializers.LocationTableSerializer - - def get_fields_for_model(self): - return self.model._meta.get_fields() - - def get(self, request, *args, **kwargs): - if request.accepted_renderer.format == 'html': - data = { - 'data' : self.get_queryset(), - 'fields': dict((field.name, field.get_internal_type()) for field in self.get_fields_for_model()) - } - return Response(data, template_name='list_rows.html') - return super(LocTableAsList, self).get(request, *args, **kwargs) - -@api_view(('GET', )) -def flightradar(request, *args, **kwargs): - return Response(f.get_flight_radar_data()) - -@api_view(('GET', )) -def traingps(request, *args, **kwargs): - return Response(t.get_train_gps_data()) diff --git a/api/manage.py b/api/manage.py deleted file mode 100644 index 8023a0a..0000000 --- a/api/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/api/requirements.txt b/api/requirements.txt deleted file mode 100644 index a2fb32a..0000000 --- a/api/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -backcall==0.1.0 -certifi==2021.10.8 -chardet==3.0.4 -charset-normalizer==2.0.7 -decorator==4.4.0 -Django==2.2.24 -djangorestframework==3.11.2 -djangorestframework-gis==0.14 -idna==3.3 -ipython==7.8.0 -ipython-genutils==0.2.0 -jedi==0.15.1 -parso==0.5.1 -pexpect==4.7.0 -pickleshare==0.7.5 -prompt-toolkit==2.0.9 -psycopg2-binary==2.8.3 -ptyprocess==0.6.0 -Pygments==2.7.4 -pytz==2019.2 -requests==2.26.0 -six==1.12.0 -sqlparse==0.3.0 -traitlets==4.3.2 -unicodecsv==0.14.1 -urllib3==1.26.7 -wcwidth==0.1.7 diff --git a/api/sync/__init__.py b/api/sync/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/sync/datasync.py b/api/sync/datasync.py deleted file mode 100644 index 352c426..0000000 --- a/api/sync/datasync.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -import io -import psycopg2 -import requests -import shutil -import tempfile -import csv #unicodecsv -import time -import zipfile - -from io import StringIO -import sys, os - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -os.environ['DJANGO_SETTINGS_MODULE'] = 'api.settings' - -import django -django.setup() - - -from django.db import connections as CONNECTIONS -from conf.settings import DBTABLES, DBSCHEMA - -ZIPURL = 'http://www.peatus.ee/gtfs/gtfs.zip' -# FIXME: ZIPURL should be in conf.settings aswell! - -class FilteredCSVFile(csv.DictReader, object): - """Local helper for reading only specified columns from a csv file. - - It's assumed that row number 1 is the header row. - """ - def __init__( - self, csvfile, fieldnames=None, restkey=None, restval=None, - dialect='excel', *args, **kwargs): - self._header = self.get_csv_header(csvfile) - super(FilteredCSVFile, self).__init__( - csvfile, self._header , restkey, restval, - dialect, *args, **kwargs) - self._fieldnames = fieldnames - - def get_csv_header(self, fp): - return fp.readline().strip('\n').split(',') - - def cleanup(self, obj): - if obj == None or obj == "": - obj = '\\N' - if isinstance(obj, str): - obj = obj.replace('\t', ' ') - if ',' in obj: - obj = '"%s"' % obj - return obj - - def next(self): - row = dict(zip(self._header, next(self.reader))) - return '\t'.join(['%s' % self.cleanup(row[k]) for k in self._fieldnames]) - - def readline(self): - return self.next() - - def read(self): - o = io.StringIO() - try: - while True: - row = self.next() - o.write(row) - o.write(u'\n') - except StopIteration as si: - pass - return o.getvalue() - - -def download_zip(url, to_path): - """Download zipfile from url and extract it to to_path. - - Returns the path of extraction. - """ - filename = url.split('/')[-1] - r = requests.get(url) - r.raise_for_status() - content = io.BytesIO(r.content) - with zipfile.ZipFile(content) as z: - z.extractall(to_path) - return to_path - -def get_csv_header(filepath): - """Retuns csv file's header row.""" - with open(filepath) as n: - return n.readline().strip('\n').split(',') - -def _db_check_table(cursor, dbschema, dbtable): - """Checks input table's existance in the database. - - Returns a list with table's column names. - """ - tab = '%s.%s' % (dbschema, dbtable) - sql = "select array_agg(attname) from pg_attribute " \ - "where attrelid=%s::regclass and not attisdropped and attnum > 0" - params = (tab,) - cursor.execute(sql, params) - return cursor.fetchone()[0] - -def _fs_check_csv(path, filename, ext='txt'): - """Checks if the input csv file really exists. - - Returns a tuple of csv absolute filepath, and headers. - """ - filename = '%s.%s' % (filename, ext) - fp = os.path.join(path, filename) - assert os.path.exists(fp) - return fp, get_csv_header(fp) - -def _get_insert_cols(db_cols, fp_cols, dbschema, tablename): - """Returns intersection of input column names. - - Use this to figure out which columns need to be read from the csv file. - """ - cols = list(set(db_cols).intersection(fp_cols)) - assert len(cols) > 0, "%s.%s and %s.csv do not share any columns" % ( - dbschema, dbtable, dbtable) - return cols - -def _db_prepare_truncate(tableschema, tablename): - """Prepare a truncate statement for a table in the database. - - @FIXME: as this is prone to injection check whether the tablename - mentioned in args really exists. - """ - sql = """truncate table %(sch)s.%(tab)s cascade""" - params = dict(sch=tableschema, tab=tablename) - return sql % params - -#{main - -def run(): - """Run data download and database sync operations""" - try: - # go get all csv files extracted at to_path. - # local - # to_path = 'tmp' - # the real thing - to_path = download_zip(ZIPURL, tempfile.mkdtemp(prefix='eoy_')) - print(to_path) - # loop through required files and look for a matching table - # in the database - # if found truncate it and insert new rows from the csv file - # if table not found, raise exception - # if exception, then rollback and stop whatever was going on - # all database commands run in a single transaction - c = CONNECTIONS['sync'] - with c.cursor() as cursor: - # loop through the list of tables specified at - # conf.settings.DBTABLES - for dbtable in DBTABLES: - # check if table exists in db and get it's columns - db_cols = _db_check_table(cursor, DBSCHEMA, dbtable) - # check if file present and get csv header - fp, fp_cols = _fs_check_csv(to_path, dbtable) - print ('%s.%s' %(DBSCHEMA, dbtable)) - # get intersection of db_cols and fp_cols (i.e cols that - # are present in both) - cols = _get_insert_cols(db_cols, fp_cols, DBSCHEMA, dbtable) - # truncate old data, - st_trunc = _db_prepare_truncate(DBSCHEMA, dbtable) - cursor.execute(st_trunc) - # and fill anew ... - with open(fp, encoding='utf-8') as f: - fcsv = FilteredCSVFile(f, fieldnames=cols, quotechar='"') - tab = '%s.%s' % (DBSCHEMA, dbtable) - cursor.copy_from(io.StringIO(fcsv.read()), tab, sep='\t', columns=cols) - print(cursor.rowcount) - print('done %s' % fp) - except: - raise - # FIXME: This is the place for calling data prep functions in the database. - - # keep the file for now... - #shutil.rmtree(to_path) - - def postprocess(): - with open('preprocess-all.sql') as f: - statements = f.read() - c = CONNECTIONS['sync'] - with c.cursor() as cursor: - for statement in statements.split(';'): - c.execute(statement) - - -if __name__ == '__main__': - run() - #pass diff --git a/api/sync/preprocess-all.sql b/api/sync/preprocess-all.sql deleted file mode 100644 index 1cc5877..0000000 --- a/api/sync/preprocess-all.sql +++ /dev/null @@ -1,203 +0,0 @@ -drop view if exists gtfs.loctable_v2 -; -drop table if exists gtfs.calcshapes -; -drop table if exists gtfs.calcstopnodes -; -drop table if exists gtfs.calctrips -; -create table gtfs.calcshapes as -select - shape_id, st_makeline(array_agg(shape)) as shape, - st_collect(shape) as nodes -from ( - select - s.shape_id, st_setsrid(st_makepoint(s.shape_pt_lon, s.shape_pt_lat), 4326) as shape - from - gtfs.shapes s - order by s.shape_id, s.shape_pt_sequence) n -group by shape_id -order by shape_id; -alter table gtfs.calcshapes - add constraint pk__calcshapes primary key (shape_id); -create index sidx__calcshapes - on gtfs.calcshapes using gist (shape); -create index sidx__calcshapes_nodes - on gtfs.calcshapes using gist (shape); -create table gtfs.calcstopnodes as -select - shape_id, st_multi(st_collect(stop_node)) as stop_nodes, - trip_id, array_agg(stop_seq) as stop_seq -from ( - select - s.shape_id, - st_closestpoint(s.nodes, st_setsrid( - st_point(stops.stop_lon, stops.stop_lat), 4326)) as stop_node, - t.trip_id, st.stop_sequence as stop_seq - from - gtfs.calcshapes s, - gtfs.stop_times st, - gtfs.trips t, - gtfs.stops stops - where - st.stop_id = stops.stop_id and - st.trip_id = t.trip_id and - s.shape_id = t.shape_id - order by s.shape_id, t.trip_id, st.stop_sequence) m -group by shape_id, trip_id -; -alter table gtfs.calcstopnodes - add constraint pk__calcstopnodes primary key (trip_id) -; -create index sidx__calcstopnodes - on gtfs.calcstopnodes using gist (stop_nodes) -; -create table gtfs.calctrips as -with - splits as ( - select - cs.shape_id, sn.trip_id, - sn.stop_seq, - (st_dump(split_line_multipoint(cs.shape, sn.stop_nodes))).* - from - gtfs.calcshapes cs, - gtfs.calcstopnodes sn - where - cs.shape_id = sn.shape_id - ), - inbetween as ( - select - splits.shape_id, splits.trip_id, - splits.stop_seq[splits.path[1]] as from_stop, - splits.stop_seq[splits.path[1]+1] as to_stop, - splits.geom as shape - from splits - ), - triptimes as ( - select - trip_id, min(departure_time) as trip_start, - max(arrival_time) as trip_fin - from gtfs.stop_times - group by trip_id - ) -select - inbetween.shape_id, inbetween.trip_id, inbetween.shape, - triptimes.trip_start, triptimes.trip_fin, - inbetween.from_stop as from_stop_seq, - from_stops.stop_id as from_stop_id, - from_stops.departure_time as from_stop_time, - inbetween.to_stop as to_stop_seq, - to_stops.stop_id as to_stop_id, - to_stops.arrival_time as to_stop_time -from - inbetween, - gtfs.stop_times from_stops, - gtfs.stop_times to_stops, - triptimes -where - inbetween.trip_id = from_stops.trip_id and - inbetween.trip_id = to_stops.trip_id and - inbetween.from_stop = from_stops.stop_sequence and - inbetween.to_stop = to_stops.stop_sequence and - triptimes.trip_id = inbetween.trip_id -order by inbetween.trip_id, inbetween.from_stop -; -create index sidx__calctrips - on gtfs.calctrips using gist (shape) -; -create or replace view gtfs.loctable_v2 as -with - curtime as ( - select - clock_timestamp()::date AS cd, - to_char(clock_timestamp(), 'hh24:mi:ss'::text) AS ct, - date_part('dow'::text, clock_timestamp()) + 1 AS d, - lpad((to_char(clock_timestamp(), 'hh24')::int + 24)::varchar,2,'0')||':'||to_char(clock_timestamp(), 'mi:ss') as plushours - ), - cal as ( - select - c.service_id - from - gtfs.calendar c, - curtime - where - curtime.cd >= c.start_date and - curtime.cd <= c.end_date and - (array[ - c.monday, - c.tuesday, - c.wednesday, - c.thursday, - c.friday, - c.saturday, - c.sunday])[curtime.d] = true - ), - startstop as ( - select - calctrips.trip_id, calctrips.from_stop_time as leg_start, - calctrips.to_stop_time as leg_fin, calctrips.from_stop_id, - calctrips.to_stop_id, calctrips.from_stop_seq, - calctrips.to_stop_seq, calctrips.shape, - calctrips.trip_start, calctrips.trip_fin - from - gtfs.calctrips, curtime - where - ( - curtime.ct >= calctrips.from_stop_time::text and - curtime.ct <= calctrips.to_stop_time::text - ) or ( - curtime.plushours >= calctrips.from_stop_time::text and - curtime.plushours <= calctrips.to_stop_time::text - ) - ), - trip as ( - select - startstop.trip_id, trips.shape_id, - startstop.trip_start, startstop.trip_fin, - startstop.leg_start, startstop.leg_fin, - curtime.ct as cur, trips.trip_headsign, - trips.trip_long_name, routes.route_short_name, - routes.route_long_name, routes.route_color, - startstop.from_stop_id, startstop.to_stop_id, - startstop.from_stop_seq, startstop.to_stop_seq, - startstop.shape - from - cal, curtime, startstop, gtfs.trips trips, gtfs.routes routes - where - trips.trip_id = startstop.trip_id and - trips.service_id = cal.service_id and - trips.route_id::text = routes.route_id::text - ) -select - trip.trip_id, trip.shape_id, trip.trip_start, trip.trip_fin, - trip.trip_headsign, trip.trip_long_name, trip.route_short_name, - trip.route_long_name, - tostop.stop_id as next_stop_id, - tostop.stop_name as next_stop, - trip.leg_fin as next_stop_time, - fromstop.stop_id as prev_stop_id, - fromstop.stop_name as prev_stop, - trip.leg_start as prev_stop_time, - curtime.ct as current_time, - '#'::text || trip.route_color::text as route_color, - /* @tkardi 09.11.2021 st_flipcoordinates to quickly get API geojson - coors order correct. FIXME: should be a django version gdal version thing. - */ - st_flipcoordinates( - st_lineinterpolatepoint( - trip.shape, - gtfs.get_time_fraction( - trip.leg_start, - trip.leg_fin, - gtfs.get_current_impeded_time( - trip.leg_start, - trip.leg_fin, - trip.cur::character varying - ) - ) - ) - ) as pos -from curtime, trip - left join gtfs.stops tostop on trip.to_stop_id = tostop.stop_id - left join gtfs.stops fromstop on trip.from_stop_id = fromstop.stop_id -; diff --git a/api/templates/list_rows.html b/api/templates/list_rows.html deleted file mode 100644 index c563a4c..0000000 --- a/api/templates/list_rows.html +++ /dev/null @@ -1,162 +0,0 @@ -{% load staticfiles %} -{% load i18n %} -{% load rest_framework %} -{% load eoy_extras %} - - - - - {% block head %} - - {% block meta %} - - - {% endblock %} - - {% block title %}{% if title %}{{ title }} – {% endif %}this is a webpage{% endblock %} - - {% block style %} - {% block bootstrap_theme %} - - - - {% endblock %} - - - - {% endblock %} - - {% endblock %} - - - - - {% block body %} - -

{{ title }}

- {% include "map.html" %} -
-
- - - {% if uri_field %}{% endif %} - {% for field_name, type in fields.items %} - {% if type not in 'GeometryField,PointField' %} - - {% endif %} - {% endfor %} - -
uri - {{ field_name }} -
-
-
- - {% for row in data %} - - {% if uri_field %}{% endif %} - {% for field_name, type in fields.items %} - {% if type not in 'GeometryField,PointField' %} - - {% endif %} - {% endfor %} - - {% endfor %} -
view - {% if type == 'DateTimeField' %} - {{ row|get_value:field_name|date:'Y-m-d H:i:s'}} - {% else %} - {{ row|get_value:field_name|default_if_none:" " }} - {% endif %} -
-
-
- {% block script %} - - - - - - - - {% endblock %} - - {% endblock %} - diff --git a/api/templates/map.html b/api/templates/map.html deleted file mode 100644 index 79638ef..0000000 --- a/api/templates/map.html +++ /dev/null @@ -1,173 +0,0 @@ -{% load static %} -{% load eoy_extras %} - - - - - - - - - -
- -
- - - diff --git a/api/templatetags/extra_templatetags.py b/api/templatetags/extra_templatetags.py deleted file mode 100644 index b185459..0000000 --- a/api/templatetags/extra_templatetags.py +++ /dev/null @@ -1,29 +0,0 @@ -from django import template - -register = template.Library() - -@register.filter -def get_value(obj, key): - return getattr(obj, key, None) - -@register.filter -def get_item(dictionary, key): - return dictionary.get(key) - -@register.filter -def get_uri(dictionary): - if hasattr(dictionary, 'get'): - return dictionary.get("@id") - -@register.simple_tag -def query_transform(request, **kwargs): - params = request.GET.copy() - params.update(kwargs) - return '?%s' % params.urlencode() if params else '' - -@register.filter -def to_geojson(obj, key): - geom = getattr(obj, key, None) - if geom != None and hasattr(geom, 'geojson'): - return geom.geojson - return None diff --git a/db/init.sql b/db/init.sql deleted file mode 100644 index 6e3e204..0000000 --- a/db/init.sql +++ /dev/null @@ -1,395 +0,0 @@ -create schema if not exists gtfs authorization postgres; -comment on schema gtfs is 'Schema for GTFS data'; - -/* Tables */ - -drop table if exists gtfs.agency; -create table gtfs.agency ( - agency_id integer, - agency_name character varying(250), - agency_url character varying(250), - agency_timezone character varying(100), - agency_phone character varying(100), - agency_lang character varying(3) -); - -alter table gtfs.agency owner to postgres; -alter table gtfs.agency add constraint - pk__agency primary key (agency_id); - - -drop table if exists gtfs.calendar; -create table gtfs.calendar( - service_id integer, - monday boolean, - tuesday boolean, - wednesday boolean, - thursday boolean, - friday boolean, - saturday boolean, - sunday boolean, - start_date date, - end_date date -); -alter table gtfs.calendar owner to postgres; -alter table gtfs.calendar add constraint - pk__calendar primary key (service_id); - - -drop table if exists gtfs.routes; -create table gtfs.routes( - route_id character varying(32), - agency_id integer, - route_short_name character varying(100), - route_long_name character varying(250), - route_type smallint, - route_color character varying(10), - competent_authority character varying(100) -); -alter table gtfs.routes owner to postgres; -alter table gtfs.routes add constraint - pk__routes primary key (route_id); ---alter table gtfs.routes add constraint --- fk__routes__agency foreign key (agency_id) references gtfs.agency (agency_id) --- on update cascade on delete no action --- deferrable initially deferred; - - -drop table if exists gtfs.shapes; -create table gtfs.shapes( - shape_id integer, - shape_pt_lat numeric, - shape_pt_lon numeric, - shape_pt_sequence smallint -); -alter table gtfs.shapes owner to postgres; -create unique index uidx__shapes on gtfs.shapes (shape_id, shape_pt_sequence); - -drop table if exists gtfs.stops; -create table gtfs.stops ( - stop_id integer, - stop_code character varying(100), - stop_name character varying(250), - stop_lat numeric, - stop_lon numeric, - zone_id integer, - alias character varying(250), - stop_area character varying(250), - stop_desc character varying(250), - lest_x numeric, - lest_y numeric, - zone_name character varying(250) -); -alter table gtfs.stops owner to postgres; -alter table gtfs.stops add constraint - pk__stops primary key (stop_id); - -drop table if exists gtfs.trips; -create table gtfs.trips ( - route_id character varying(32), - service_id integer, - trip_id integer, - trip_headsign character varying(250), - trip_long_name character varying(250), - direction_code character varying(10), - shape_id integer, - wheelchair_accessible boolean -); -alter table gtfs.trips owner to postgres; - - -drop table if exists gtfs.stop_times; -create table gtfs.stop_times( - trip_id integer, - arrival_time character varying(8), - departure_time character varying(8), - stop_id integer, - stop_sequence smallint, - pickup_type smallint, - drop_off_type smallint -); -alter table gtfs.stop_times owner to postgres; ---alter table gtfs.stop_times add constraint --- fk__stop_times__stops foreign key (stop_id) references gtfs.stops (stop_id) --- on update cascade on delete no action --- deferrable initially deferred; - - -/* Functions */ - - -create or replace function gtfs.get_time_fraction( - trip_start varchar, trip_fin varchar, curtime varchar) -returns numeric as -$$ -declare - a_cur int; - a_strt int; - a_fin int; - strt time; - fin interval; - totalsecs numeric := 1; - fractionsecs numeric := 0; -begin - --a_fin := string_to_array(trip_fin, ':'); - a_fin := extract(epoch from trip_fin::interval); - --a_strt := string_to_array(trip_start, ':'); - a_strt := extract(epoch from trip_start::interval); - --a_cur := string_to_array(curtime, ':'); - a_cur := extract(epoch from curtime::interval); - if a_cur < a_strt then - a_cur := a_cur + 24*60*60; - end if; - if a_cur between a_strt and a_fin then - fractionsecs := (a_cur::numeric-a_strt::numeric); - totalsecs := (a_fin::numeric-a_strt::numeric); - end if; - -/* if a_fin[1]::smallint >= 24 then - fin := ((a_fin[1]::smallint - 24)::varchar||':'||(a_fin)[2]||':'||(a_fin)[3])::time; - totalsecs := extract(epoch from (('24:00:00'::time - trip_start::time) + fin)); - else - totalsecs := extract(epoch from trip_fin::time - trip_start::time); - end if; - - if a_cur[1]::smallint < a_strt[1]::smallint then - fin := '24:00:00'::time - trip_start::time; - fractionsecs := extract(epoch from (curtime::time + fin)); - else - fractionsecs := extract(epoch from curtime::time - trip_start::time); - end if; - */ - --raise notice 'a_cur %', a_cur; - --raise notice 'a_strt %', a_strt; - --raise notice 'a_fin %', a_fin; - --raise notice 'Fraction %', fractionsecs; - --raise notice 'Total %', totalsecs; - return fractionsecs::numeric / totalsecs::numeric; -end; -$$ -language plpgsql -security invoker; -alter function gtfs.get_time_fraction(varchar, varchar, varchar) owner to postgres; -comment on function gtfs.get_time_fraction(varchar, varchar, varchar) is 'Calculates the relative fraction that current time represents in between start and finish timestamps.'; - - -/* ------------------------------------------------------------------- --- TESTS fro gtfs.get_time_fraction (varchar, varchar, varchar) -- ------------------------------------------------------------------- - - -select f.*, gtfs.get_time_fraction (f.str, f.fin, f.cur) as fraction, gtfs.get_time_fraction (f.str, f.fin, f.cur) = f.expected as test -from ( -select '23:00:00' as str, '24:00:00' as fin, '23:01:00' as cur, 1::numeric/60::numeric as expected -union all -select '23:00:00', '24:00:00', '23:30:00', 0.5 -union all -select '23:00:00', '25:00:00', '00:30:00', 0.75 -union all -select '23:00:00', '23:01:00', '23:01:00', 1.0 -union all -select '23:00:00', '23:01:00', '23:00:00', 0.0 -union all -select '00:10:00', '00:44:00', '00:27:00', 0.5 -union all -select '24:10:00', '24:44:00', '00:27:00', 0.5 -union all -select '22:00:00', '28:00:00', '01:00:00', 0.5 -) f; -*/ - - -create or replace function gtfs.get_current_impeded_time( - laststop varchar, nextstop varchar, curtime varchar, - stoptime integer default 10, acctime integer default 10) -returns varchar as -$$ -declare - a_cur varchar[]; - a_prev varchar[]; - a_next varchar[]; - prv numeric; - nxt numeric; - cur numeric; - dt numeric; - X numeric; - M numeric; - nxt_nextday boolean; - prv_nextday boolean; -begin - a_cur := string_to_array(curtime, ':'); - a_prev := string_to_array(laststop, ':'); - a_next := string_to_array(nextstop, ':'); - - if a_cur[1]::smallint < a_prev[1]::smallint then - -- means curtime is past midnight - cur := extract(epoch from '24:00:00'::time) + extract(epoch from curtime::time); - else - -- we are still in the same day as previous stop was - cur := extract(epoch from curtime::time); - end if; - - if a_prev[1]::smallint >= 24 then - -- previous stop was in fact tomorrow - prv := extract(epoch from '24:00:00'::time) + extract(epoch from ((a_prev[1]::smallint - 24)::varchar||':'||(a_prev)[2]||':'||(a_prev)[3])::time); - prv_nextday := true; - else - -- previous stop was today - prv := extract(epoch from laststop::time); - prv_nextday := false; - end if; - - if a_next[1]::smallint >= 24 then - -- next stop will be tomorrow - nxt := extract(epoch from '24:00:00'::time) + extract(epoch from ((a_next[1]::smallint - 24)::varchar||':'||(a_next)[2]||':'||(a_next)[3])::time); - nxt_nextday := true; - else - -- next stop will be today - nxt := extract(epoch from nextstop::time); - nxt_nextday := false; - end if; - - /* Check whether we are currently: a. stopped, b. speeding up, c. slowing down, d. going full speed */ - if (cur - prv < stoptime) then - -- stop at the prev station ->> return time at previous station as current time, - -- but check whether hours are correct and justify - if prv_nextday = false then - return laststop; - else - return ((a_prev[1]::smallint - 24)::varchar||':'||(a_prev)[2]||':'||(a_prev)[3])::time::varchar; - end if; - elsif (nxt - cur < stoptime) then - -- stop at the next station ->> return time at next station as current time, - -- but check whether hours are correct and justify - if nxt_nextday = false then - return nextstop; - else - return ((a_next[1]::smallint - 24)::varchar||':'||(a_next)[2]||':'||(a_next)[3])::time::varchar; - end if; - elsif ((cur - prv) < (stoptime + acctime)) then - -- gathering speed ->> return time with 1:1 ratio - dt := prv + (tan(radians(45)) * (cur - prv - stoptime)); - elsif ((nxt - cur) < (stoptime + acctime)) then - -- slowing down ->> return time with 1:1 ratio - X := nxt - prv; - dt := nxt - (tan(radians(45)) * (nxt - cur - stoptime)); - else - -- doing full speed ->> return whatever timespan we need to cover - X := nxt - prv; - M := acctime + stoptime; - dt := ((X - 2 * acctime)::numeric / (X - 2 * M)::numeric + 0.000001) * (cur - prv - M)::numeric; - dt := prv + acctime + dt; - end if; - return (timestamp 'epoch' + dt * interval '1 second')::time::varchar; -end; -$$ -language plpgsql -security invoker; -alter function gtfs.get_current_impeded_time(varchar, varchar, varchar, integer, integer) owner to postgres; -comment on function gtfs.get_current_impeded_time( - varchar, varchar, varchar, integer, integer -) is 'Calculates current "impeded time" based on last and next stoptimes and current real time as described in https://github.com/tkardi/eoy/issues/2'; - - -/* - ------------------------------------------------------------------- --- TESTS fro gtfs.get_current_impeded_time (varchar, varchar, varchar, integer, integer) -- ------------------------------------------------------------------- - -select t.*, gtfs.get_current_impeded_time(t.strt, t.fin, t.cur, 10, 10), -gtfs.get_current_impeded_time(t.strt, t.fin, t.cur, 10, 10) = t.expected as test -from ( - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'stopped' as state, - ('23:59:'||lpad(generate_series(30, 39)::varchar, 2, '0' ))::varchar as cur, - '23:59:30'::varchar as expected -union all - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'accelerating' as state, - ('23:59:'||lpad(generate_series(40, 49)::varchar, 2, '0' ))::varchar as cur, - ('23:59:'||lpad(generate_series(30, 39)::varchar, 2, '0' ))::varchar as expected -union all - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'fullspeed day 1' as state, - ('23:59:'||lpad(generate_series(50, 59)::varchar, 2, '0' ))::varchar as cur, - ('23:59:'||lpad(generate_series(40, 59, 2)::varchar, 2, '0' ))::varchar as expected -union all - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'fullspeed day 2' as state, - ('00:00:'||lpad(generate_series(0, 9)::varchar, 2, '0' ))::varchar as cur, - ('00:00:'||lpad(generate_series(0, 19, 2)::varchar, 2, '0' ))::varchar as expected -union all - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'stopping' as state, - ('00:00:'||lpad(generate_series(10, 19)::varchar, 2, '0' ))::varchar as cur, - ('00:00:'||lpad(generate_series(20, 29)::varchar, 2, '0' ))::varchar as expected -union all - select - '23:59:30'::varchar as strt, - '24:00:30'::varchar as fin, - 'stopped' as state, - ('00:00:'||lpad(generate_series(20, 29)::varchar, 2, '0' ))::varchar as cur, - '00:00:30'::varchar as expected - -) t; -*/ - -/** FUNCTION split_line_multipoint(geometry, geometry) -* by http://gis.stackexchange.com/users/564/rcoup -* posted @ http://gis.stackexchange.com/a/112317 -*/ - -CREATE OR REPLACE FUNCTION public.split_line_multipoint( - input_geom geometry, - blade geometry) - RETURNS geometry AS -$BODY$ - -- this function is a wrapper around the function ST_Split - -- to allow splitting multilines with multipoints - -- - DECLARE - result geometry; - simple_blade geometry; - blade_geometry_type text := GeometryType(blade); - geom_geometry_type text := GeometryType(input_geom); - BEGIN - IF blade_geometry_type NOT ILIKE 'MULTI%' THEN - RETURN ST_Split(input_geom, blade); - ELSIF blade_geometry_type NOT ILIKE '%POINT' THEN - RAISE NOTICE 'Need a Point/MultiPoint blade'; - RETURN NULL; - END IF; - - IF geom_geometry_type NOT ILIKE '%LINESTRING' THEN - RAISE NOTICE 'Need a LineString/MultiLineString input_geom'; - RETURN NULL; - END IF; - - result := input_geom; - -- Loop on all the points in the blade - FOR simple_blade IN SELECT (ST_Dump(ST_CollectionExtract(blade, 1))).geom - LOOP - -- keep splitting the previous result - result := ST_CollectionExtract(ST_Split(result, simple_blade), 2); - END LOOP; - RETURN result; - END; -$BODY$ - LANGUAGE plpgsql IMMUTABLE - COST 100; -ALTER FUNCTION public.split_line_multipoint(geometry, geometry) - OWNER TO postgres; -comment on function public.split_line_multipoint(geometry, geometry) is - 'Function by http://gis.stackexchange.com/users/564/rcoup posted @ http://gis.stackexchange.com/a/112317'; diff --git a/db/preprocess.sql b/db/preprocess.sql deleted file mode 100644 index 9659533..0000000 --- a/db/preprocess.sql +++ /dev/null @@ -1,249 +0,0 @@ -/** Some tuning/preprocessing. These will have to be wrapped either -* in a db function (so we can call it from datasync.py) -* or alternatively save as plaintext files and let datasync.py -* read them (it) and then execute in a transaction after data sync -* has taken place. -*/ - - --- drop previous -drop view if exists gtfs.loctable_v2; -drop table if exists gtfs.calcshapes; -drop table if exists gtfs.calcstopnodes; -drop table if exists gtfs.calctrips; - --- and create anew - -/** table: gtfs.calcshapes -* -* A table for storing full trip shapes (linestrings) and -* collected nodes, which we'll use afterwards for "snapping" -* stops onto trip shapes. -*/ - - -create table gtfs.calcshapes as -select - shape_id, st_makeline(array_agg(shape)) as shape, - st_collect(shape) as nodes -from ( - select - s.shape_id, st_setsrid(st_makepoint(s.shape_pt_lon, s.shape_pt_lat), 4326) as shape - from - gtfs.shapes s - order by s.shape_id, s.shape_pt_sequence) n -group by shape_id -order by shape_id; - -alter table gtfs.calcshapes - add constraint pk__calcshapes primary key (shape_id); -create index sidx__calcshapes - on gtfs.calcshapes using gist (shape); -create index sidx__calcshapes_nodes - on gtfs.calcshapes using gist (shape); -alter table gtfs.calcshapes owner to postgres; - - -/** table: gtfs:calcstopnodes -* -* Stores closest nodes on trip shapes to respective stops. -*/ - - -create table gtfs.calcstopnodes as -select - shape_id, st_multi(st_collect(stop_node)) as stop_nodes, - trip_id, array_agg(stop_seq) as stop_seq -from ( - select - s.shape_id, - st_closestpoint(s.nodes, st_setsrid( - st_point(stops.stop_lon, stops.stop_lat), 4326)) as stop_node, - t.trip_id, st.stop_sequence as stop_seq - from - gtfs.calcshapes s, - gtfs.stop_times st, - gtfs.trips t, - gtfs.stops stops - where - st.stop_id = stops.stop_id and - st.trip_id = t.trip_id and - s.shape_id = t.shape_id - order by s.shape_id, t.trip_id, st.stop_sequence) m -group by shape_id, trip_id; - -alter table gtfs.calcstopnodes - add constraint pk__calcstopnodes primary key (trip_id); -create index sidx__calcstopnodes - on gtfs.calcstopnodes using gist (stop_nodes); -alter table gtfs.calcstopnodes owner to postgres; - - -/** table: gtfs.calctrips -* -* Break trip shapes up into shorter linestrings based on stops (i.e -* "trip shape closest nodes to stops"). Call these "trip legs". -* A trip leg starts at gtfs.stop_times.departure_time and ends at -* gtfs.stop_times.arrival_time. -*/ - - -create table gtfs.calctrips as -with - splits as ( - select - cs.shape_id, sn.trip_id, - sn.stop_seq, - (st_dump(split_line_multipoint(cs.shape, sn.stop_nodes))).* - from - gtfs.calcshapes cs, - gtfs.calcstopnodes sn - where - cs.shape_id = sn.shape_id - ), - inbetween as ( - select - splits.shape_id, splits.trip_id, - splits.stop_seq[splits.path[1]] as from_stop, - splits.stop_seq[splits.path[1]+1] as to_stop, - splits.geom as shape - from splits - ), - triptimes as ( - select - trip_id, min(departure_time) as trip_start, - max(arrival_time) as trip_fin - from gtfs.stop_times - group by trip_id - ) -select - inbetween.shape_id, inbetween.trip_id, inbetween.shape, - triptimes.trip_start, triptimes.trip_fin, - inbetween.from_stop as from_stop_seq, - from_stops.stop_id as from_stop_id, - from_stops.departure_time as from_stop_time, - inbetween.to_stop as to_stop_seq, - to_stops.stop_id as to_stop_id, - to_stops.arrival_time as to_stop_time -from - inbetween, - gtfs.stop_times from_stops, - gtfs.stop_times to_stops, - triptimes -where - inbetween.trip_id = from_stops.trip_id and - inbetween.trip_id = to_stops.trip_id and - inbetween.from_stop = from_stops.stop_sequence and - inbetween.to_stop = to_stops.stop_sequence and - triptimes.trip_id = inbetween.trip_id -order by inbetween.trip_id, inbetween.from_stop; - ---alter table gtfs.calctrips add constraint pk__calcstopnodes primary key (trip_id); -create index sidx__calctrips - on gtfs.calctrips using gist (shape); -alter table gtfs.calctrips owner to postgres; - - -/** view: gtfs.loctable_v2 -* -* Estimated locations of currently running public transport. This -* is the view that will be queried for current location of public transit -* vehicles. -*/ - - -create or replace view gtfs.loctable_v2 as -with - curtime as ( - select - clock_timestamp()::date AS cd, - to_char(clock_timestamp(), 'hh24:mi:ss'::text) AS ct, - date_part('dow'::text, clock_timestamp()) + 1 AS d, - lpad((to_char(clock_timestamp(), 'hh24')::int + 24)::varchar,2,'0')||':'||to_char(clock_timestamp(), 'mi:ss') as plushours - ), - cal as ( - select - c.service_id - from - gtfs.calendar c, - curtime - where - curtime.cd >= c.start_date and - curtime.cd <= c.end_date and - (array[ - c.monday, - c.tuesday, - c.wednesday, - c.thursday, - c.friday, - c.saturday, - c.sunday])[curtime.d] = true - ), - startstop as ( - select - calctrips.trip_id, calctrips.from_stop_time as leg_start, - calctrips.to_stop_time as leg_fin, calctrips.from_stop_id, - calctrips.to_stop_id, calctrips.from_stop_seq, - calctrips.to_stop_seq, calctrips.shape, - calctrips.trip_start, calctrips.trip_fin - from - gtfs.calctrips, curtime - where - ( - curtime.ct >= calctrips.from_stop_time::text and - curtime.ct <= calctrips.to_stop_time::text - ) or ( - curtime.plushours >= calctrips.from_stop_time::text and - curtime.plushours <= calctrips.to_stop_time::text - ) - ), - trip as ( - select - startstop.trip_id, trips.shape_id, - startstop.trip_start, startstop.trip_fin, - startstop.leg_start, startstop.leg_fin, - curtime.ct as cur, trips.trip_headsign, - trips.trip_long_name, routes.route_short_name, - routes.route_long_name, routes.route_color, - startstop.from_stop_id, startstop.to_stop_id, - startstop.from_stop_seq, startstop.to_stop_seq, - startstop.shape - from - cal, curtime, startstop, gtfs.trips trips, gtfs.routes routes - where - trips.trip_id = startstop.trip_id and - trips.service_id = cal.service_id and - trips.route_id::text = routes.route_id::text - ) -select - trip.trip_id, trip.shape_id, trip.trip_start, trip.trip_fin, - trip.trip_headsign, trip.trip_long_name, trip.route_short_name, - trip.route_long_name, - tostop.stop_id as next_stop_id, - tostop.stop_name as next_stop, - trip.leg_fin as next_stop_time, - fromstop.stop_id as prev_stop_id, - fromstop.stop_name as prev_stop, - trip.leg_start as prev_stop_time, - curtime.ct as current_time, - '#'::text || trip.route_color::text as route_color, - /* @tkardi 09.11.2021 st_flipcoordinates to quickly get API geojson - coors order correct. FIXME: should be a django version gdal version thing. - */ - st_lineinterpolatepoint( - trip.shape, - gtfs.get_time_fraction( - trip.leg_start, - trip.leg_fin, - gtfs.get_current_impeded_time( - trip.leg_start, - trip.leg_fin, - trip.cur::character varying - ) - ) - ) as pos -from curtime, trip - left join gtfs.stops tostop on trip.to_stop_id = tostop.stop_id - left join gtfs.stops fromstop on trip.from_stop_id = fromstop.stop_id; - -alter table gtfs.loctable_v2 owner to postgres; diff --git a/db/scrap/kiirendus.png b/db/scrap/kiirendus.png deleted file mode 100644 index eaa65abf6f162df0676e48eaca1216c86e2e6d75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65411 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

|}esLcDlI+1{JmtMD}Y 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 Y1daY+HWy6KBLTkd>6z))YdgL8e-6{cUH||9 diff --git a/db/scrap/qtests.sql b/db/scrap/qtests.sql deleted file mode 100644 index ccfa432..0000000 --- a/db/scrap/qtests.sql +++ /dev/null @@ -1,101 +0,0 @@ -with - startstop as ( - select trip_id, min(arrival_time) as trip_start, max(departure_time) as trip_fin - from gtfs.stop_times - group by trip_id - ), - curtime as ( - select clock_timestamp()::date as cd, - to_char(clock_timestamp(), 'hh24:mi:ss') as ct, - extract (dow from clock_timestamp()) as d - ), - cal as ( - select c.service_id - from gtfs.calendar c, curtime - where - curtime.cd between c.start_date and c.end_date and - (array[c.monday, c.tuesday, c.wednesday, c.thursday, c.friday, c.saturday, c.sunday])[extract (dow from cd)] = true - ), - trip as ( - select - startstop.trip_id, - trips.shape_id, - gtfs.get_time_fraction(startstop.trip_start, startstop.trip_fin, curtime.ct) as fraction, - startstop.trip_start as strt, - startstop.trip_fin as fin, - curtime.ct as cur, - trips.trip_headsign, - trips.trip_long_name, - routes.route_short_name, - routes.route_long_name, - routes.route_color - from cal, curtime, startstop, gtfs.trips trips, gtfs.routes routes - where - startstop.trip_start < curtime.ct and - startstop.trip_fin > curtime.ct and - trips.trip_id = startstop.trip_id and - trips.service_id = cal.service_id and - trips.route_id = routes.route_id - ), - nextstop as ( - select - n.trip_id, s.stop_id, s.stop_lon, s.stop_lat, s.stop_name, - st.arrival_time, st.departure_time, st.stop_sequence - from - gtfs.stops s, gtfs.stop_times st, - (select - trip.trip_id, min(st.stop_sequence) as seq - from - gtfs.stop_times st, trip, curtime - where - st.trip_id = trip.trip_id and - st.arrival_time > curtime.ct - group by trip.trip_id) n - where - s.stop_id = st.stop_id and - n.trip_id = st.trip_id and - n.seq = st.stop_sequence - ), - prevstop as ( - select - n.trip_id, s.stop_id, s.stop_lon, s.stop_lat, s.stop_name, - st.arrival_time, st.departure_time, st.stop_sequence - from - gtfs.stops s, gtfs.stop_times st, - (select - trip.trip_id, max(st.stop_sequence) as seq - from - gtfs.stop_times st, trip, curtime - where - st.trip_id = trip.trip_id and - st.arrival_time < curtime.ct - group by trip.trip_id) n - where - s.stop_id = st.stop_id and - n.trip_id = st.trip_id and - n.seq = st.stop_sequence - ), - shp as ( - select shape_id, st_makeline(array_agg(shape)) as shape - from ( - select s.shape_id, st_setsrid(st_makepoint(s.shape_pt_lon, s.shape_pt_lat), 4326) as shape - from gtfs.shapes s, trip t - where t.shape_id = s.shape_id - order by s.shape_id, s.shape_pt_sequence) n - group by shape_id - ) -select - trip.trip_id, trip.shape_id, trip.strt, trip.fin, trip.cur, - nextstop.stop_id, nextstop.stop_lon, nextstop.stop_lat, nextstop.stop_name, - nextstop.arrival_time, nextstop.departure_time, nextstop.stop_sequence, - prevstop.stop_id, prevstop.stop_lon, prevstop.stop_lat, prevstop.stop_name, - prevstop.arrival_time, prevstop.departure_time, prevstop.stop_sequence, - trip.trip_headsign, trip.trip_long_name, - trip.route_short_name, trip.route_long_name, - '#'||trip.route_color as route_color, - st_lineinterpolatepoint(shp.shape, trip.fraction) as pos -from shp, trip, nextstop, prevstop -where - trip.shape_id = shp.shape_id and - nextstop.trip_id = trip.trip_id and - prevstop.trip_id = trip.trip_id;