Compare commits

..

117 Commits

Author SHA1 Message Date
190985d42c Update README.md 2021-01-26 17:28:14 +00:00
7695a6838f Activate strict CORS 2021-01-17 15:38:31 +00:00
tim-herbst
2f9b7f48bc adjust popup-anchor in for map-station-popup 2021-01-17 11:18:48 +01:00
4b91b4401f Merge branch 'frontend-bugfix' 2021-01-15 12:24:27 +01:00
b0741fc0fe Change color and performance in table 2021-01-15 12:24:15 +01:00
b9c628b505 Change auto refresh toggle color 2021-01-15 12:23:39 +01:00
5bc58b8df9 Merge branch 'frontend' 2021-01-14 02:05:42 +01:00
77444dec89 Add Favicon and remove outline 2021-01-12 22:55:46 +01:00
cf6ab406f4 Layouting stuff for the user input 2021-01-12 01:11:22 +01:00
94c3fb99f4 Fix loading animation, shorter wording for dashboard map legend 2021-01-11 22:37:14 +01:00
1545ce1a78 Some final touches Issue #4
* Wrap tables in mat cards
* Add accidents legend
* Adjust table column names
* Some CSS adjustments
2021-01-11 22:22:26 +01:00
tim-herbst
47d53ecc54 refactor copyright to match alphabetical order of devs 2021-01-10 16:41:18 +01:00
tim-herbst
2edc3b0a83 remove last duplicates 2021-01-10 16:36:28 +01:00
tim-herbst
83e136b61c add footer-component to add copyright 2021-01-10 16:36:10 +01:00
tim-herbst
a8bff8965c refactor toolbar to match modern code patterns and make app to a god damn spa
* outsource toolbar to own component
* ngIf to listen to activated route => display user-interaction based on route
* remove unecessary code
2021-01-10 14:59:37 +01:00
860d351323 Extract common code for table loading
Preselect dashboard station in table selection model
2021-01-08 14:56:39 +01:00
tim-herbst
b6b02f2c01 adjust Title and browser-tab title and add white bike-logo 2021-01-08 14:26:05 +01:00
9686dc2ea5 Adjust styling and wording in some places 2021-01-08 12:36:03 +01:00
493cdd9ea7 Use promise.all in table component to sync the table loading 2021-01-08 12:35:09 +01:00
tim-herbst
abfc5424a3 fix error with auto-refresh
* if layer is toggled with auto-refresh, the layer will be drawn after refresh
2021-01-03 15:16:44 +01:00
tim-herbst
8ef1b34c3b dynamically add and remove legend with checkbox-toggle 2021-01-03 14:45:14 +01:00
tim-herbst
210061442f fix error with color for polyline after change of date-span 2021-01-03 14:21:22 +01:00
89d4738da5 Add min width and add some more margins 2021-01-03 04:41:23 +01:00
8724eaf914 Remove some more weird fxFlex and fxLayout directives 2021-01-03 04:33:31 +01:00
c73d99bc51 God damn, it's responsive now! 2021-01-03 04:18:00 +01:00
tim-herbst
52e68a4f28 add progress-spinner to charts 2021-01-02 23:53:30 +01:00
tim-herbst
c91a0f9872 fix display misconsumption in map-legend from cube to line 2021-01-02 23:26:04 +01:00
tim-herbst
f2dd47684b add spinner to table 2021-01-02 23:21:56 +01:00
tim-herbst
9c38fe4c76 finish work on polyline
* add legend
2021-01-02 23:17:26 +01:00
tim-herbst
a3bf06075e Revert "centralize dashboardInit fetch to parent-component and give childs necessary input to reduce redundant api calls"
This reverts commit dcd5cb72c5.
2021-01-02 18:11:21 +01:00
tim-herbst
dcd5cb72c5 centralize dashboardInit fetch to parent-component and give childs necessary input to reduce redundant api calls 2021-01-02 18:04:14 +01:00
tim-herbst
bd21861368 fix loading error on chart rental-time 2021-01-02 16:29:06 +01:00
tim-herbst
16468ba950 refactor miniMap to component 2021-01-02 16:28:43 +01:00
tim-herbst
e6f5407319 refactor user-input to component 2021-01-02 16:08:01 +01:00
tim-herbst
cdea238830 refactor rent-time to component 2021-01-02 15:33:11 +01:00
tim-herbst
6d7c40ada6 refactor rent-duration to component 2021-01-02 14:48:58 +01:00
tim-herbst
cdbf36fadb add refactor tables to component 2021-01-02 14:48:28 +01:00
tim-herbst
9c53c382ff disable checkbox for dashboard-station 2021-01-01 20:19:39 +01:00
tim-herbst
7a80335860 add popUp to miniMap for better usability 2021-01-01 16:33:48 +01:00
tim-herbst
82b8b8bd74 add title to xaxis 2021-01-01 14:59:22 +01:00
tim-herbst
01e47b9656 add description to charts and maybe finish work on code? 2021-01-01 14:47:35 +01:00
tim-herbst
5e4952b08e finish work on color-picking algorithm 2021-01-01 13:20:59 +01:00
tim-herbst
f5924404a7 mea culpa mi dominus 2020-12-30 15:26:11 +01:00
tim-herbst
c05ee5388e adjust margin for auto-refresh 2020-12-30 12:33:39 +01:00
tim-herbst
4525c10207 fix table and marker generation 2020-12-30 12:32:02 +01:00
tim-herbst
b9080f64b0 remove logs 2020-12-30 12:17:50 +01:00
tim-herbst
a3045f406c fix deprecated warning in dashboard-component 2020-12-30 12:16:22 +01:00
tim-herbst
8de1c8cfc3 finish work on auto-refresh
* remove stupidy
* implement it the marcel way
* fix control-duplicates
2020-12-30 12:14:23 +01:00
84c9016be4 Fix IndexError when Bikepoint doesn't exist in historical data 2020-12-29 20:07:12 +01:00
tim-herbst
5e71b3c094 adjust datepicker format 2020-12-29 15:06:50 +01:00
tim-herbst
953b75d55e fix display Issue on miniMap
* future considerations: reduce massive boilerplate code due to different tableSources
2020-12-29 14:48:19 +01:00
tim-herbst
bba88ac1aa WIP: add dynamic generation of marker if check-box in table is pressed
* fix: both use the same array -> delete if uncheck
2020-12-29 14:36:59 +01:00
tim-herbst
a597343b0c adjust tooltip in borrow-time to display average duration formatted with minutes 2020-12-29 11:07:51 +01:00
tim-herbst
1b19013b0a WIP: import mat-slide toggle 2020-12-29 10:01:09 +01:00
Tim Herbst
3f3dd14e8f humanize seconds in table 2020-12-28 11:58:22 +01:00
Tim Herbst
1b82cd6b8f add station-link navigation in tables and add default start value in range-picker 2020-12-28 11:45:53 +01:00
Tim Herbst
3600f3a6e3 rearrange user-input card and add auto-refresh component 2020-12-28 11:13:42 +01:00
Tim Herbst
8a69ffe29e add fix to solve scrollbar-problem 2020-12-28 10:50:58 +01:00
Tim Herbst
42735b4c90 update peer-dependencies to get rid of warnings 2020-12-27 20:29:59 +01:00
Tim Herbst
3b0889862c adjust drawAccidents to display accidents without clustering 2020-12-27 20:19:46 +01:00
Tim Herbst
46c5d2192a correct api-call for accidents
* possible map-fix with height/width
* adjust flex-layout dependency
2020-12-27 17:47:04 +01:00
8947f9116e Return more data on TopStations endpoints 2020-12-27 15:52:26 +01:00
Tim Herbst
9afb135d91 add panTo-method to center view on marker-click 2020-12-27 15:46:43 +01:00
Tim Herbst
b484857746 rename funcs for better understanding 2020-12-27 15:35:13 +01:00
Tim Herbst
600015d1bd add accidents layer with clustering 2020-12-27 15:25:07 +01:00
Tim Herbst
cff34ac31d replace marker-asset to match color-theme 2020-12-27 14:20:14 +01:00
Tim Herbst
4503274a2e add new theme.scss to make design like one pour 2020-12-27 14:10:47 +01:00
Tim Herbst
5358bb21db add Chart for Availability to Dashboard 2020-12-27 13:59:09 +01:00
Tim Herbst
110f8f2595 change dashboard user-input alignment 2020-12-27 11:47:56 +01:00
Tim Herbst
f1f82b9dd1 add routerLink to mat-table to later route to given Station
* adjust color to match bootstrap blue
2020-12-27 11:31:49 +01:00
13f06a0ce5 Possibly fix for heatmap 2020-12-26 21:26:29 +01:00
44da56f823 Add prod flag to ng build in package.json 2020-12-26 20:26:42 +01:00
Tim Herbst
570b397173 set IMapBikePoint to get status for meta-inf 2020-12-26 20:23:29 +01:00
Tim Herbst
59399fcc75 add final chart for popup-component 2020-12-26 19:28:28 +01:00
Tim Herbst
7372a1bc87 remove kebab-case 2020-12-26 19:28:10 +01:00
Tim Herbst
e8e6b3bb9c synchron call for maps and overlay
* implement heatmap
* adjust pupupAnchor for new popup-component
2020-12-26 19:27:51 +01:00
Tim Herbst
7ca560ec2f adjust gitignore for generated files 2020-12-26 17:09:39 +01:00
Tim Herbst
531c83db97 delete generated files 2020-12-26 17:09:11 +01:00
Tim Herbst
945642db5a add last chart: burrow-time to dashboard 2020-12-26 16:27:08 +01:00
Tim Herbst
f64fe65ed6 rearrange dashboard-components 2020-12-26 16:26:50 +01:00
Tim Herbst
02ff4267bf add leaflet.heat plugin 2020-12-26 16:26:02 +01:00
3cdfac30bc Add zeroes for timeFrames if they do not exist to dashboards time endpoint 2020-12-26 15:20:32 +01:00
Tim Herbst
b19da0c819 rearrange div elements 2020-12-26 11:06:32 +01:00
Tim Herbst
d3f40f994c revert fix with apex-chart and fix map-error: map-container already initialized 2020-12-23 14:34:16 +01:00
Tim Herbst
575dad8646 adjust overall style to match common applications 2020-12-23 14:31:44 +01:00
7a85fe933c Fix sorting of dashboard duration groups
Include all groups, even when they are zero
2020-12-23 00:10:28 +01:00
Tim Herbst
7904963b3e change bar-chart ending shape to flat 2020-12-22 23:09:58 +01:00
Tim Herbst
b6dd2ee825 change series and categories matching due to lexicographic sort of minutesGroup 2020-12-22 23:07:28 +01:00
Tim Herbst
678272ef8a working on dashboard
* borrow-duration is fix
* to and from is fix
* map-load with marker is fix

missing: dashboard-chart-borrow-time
2020-12-22 22:49:26 +01:00
Tim Herbst
8dd31f3703 add dashboard-station-to object to fill first table
* add flexLayout to display columns
2020-12-22 17:05:23 +01:00
Tim Herbst
bcdd859be9 adjust api invocation order to eliminate race-condition and easier access to necessary objects
* add start-Datepicker
* add end-Datepicker
* toggle-Sidenav
2020-12-22 16:09:26 +01:00
Tim Herbst
b51e533834 add necessary imports and adjust overall theme style 2020-12-22 16:08:00 +01:00
Tim Herbst
b3d6ee8473 start implementation of dashboard-component 2020-12-22 07:57:00 +01:00
Tim Herbst
6581b621fe change api-url to it-schwarz due to local-errors 2020-12-22 07:57:00 +01:00
Tim Herbst
281fb3ae40 add dashboard service 2020-12-22 07:57:00 +01:00
Tim Herbst
7c1612f8da rename DO to MapBikePoint for introduction of dashboard-common-station 2020-12-22 07:57:00 +01:00
Tim Herbst
1e755566e3 add dependencies for sidenav and datepicker 2020-12-22 07:57:00 +01:00
tim-herbst
af540c5f09 add chart to popUp-component
* bindPopUp on Button-click
* add url for prod-deployment
2020-12-22 07:57:00 +01:00
tim-herbst
6698381f85 add popup-component and dashboard-component for futher implementation
* add apex-chart dependency
2020-12-22 07:57:00 +01:00
tim-herbst
3eb3570370 dynamically load all markers from backend
* make correct api-call (remove dummy url)
* make correct method-invocation in mapcomponent
* remove dummy-data
* add domain-object for bikestation
2020-12-22 07:57:00 +01:00
tim-herbst
ba0f7b5e86 add routing to project 2020-12-22 07:57:00 +01:00
tim-herbst
95baf1f9b7 add Bikestation object 2020-12-22 07:57:00 +01:00
tim-herbst
22cd28e2b3 add new component to seperate content of marker-popup
* ng generate new component
* add dummy binding to give component a bike-station
2020-12-22 07:57:00 +01:00
tim-herbst
18ceed31b1 add css import for leaflet.markercluster to finish work on clustering marker 2020-12-22 07:57:00 +01:00
tim-herbst
4fb25b3750 update gitignore to exclude generated css files 2020-12-22 07:57:00 +01:00
tim-herbst
bb5921fe15 add marker-clustering support 2020-12-22 07:57:00 +01:00
tim-herbst
82c5c3e3c0 add marker asset and make Marker call with dummy data for proof of concept 2020-12-22 07:57:00 +01:00
tim-herbst
4e46674c07 add link to wiki in toolbar
* adjust margin and size of map-frame
* add link to gitlab wiki
2020-12-22 07:57:00 +01:00
tim-herbst
f69c999f02 add MapService to project
* refactor current mapinit to service
2020-12-22 07:57:00 +01:00
tim-herbst
711f3457b6 align map 2020-12-22 07:57:00 +01:00
tim-herbst
ab4ffaab87 add flex module to project 2020-12-22 07:57:00 +01:00
tim-herbst
a72a454f3d add angular material to project and implement first mat-toolbar for first screen design 2020-12-22 07:57:00 +01:00
tim-herbst
6b0a804aa0 add map-component
* display london
* do it the Schwarz way ;)
2020-12-22 07:57:00 +01:00
tim-herbst
b140b1b6bd add leaflet dependency to project 2020-12-22 07:57:00 +01:00
tim-herbst
064bd86f2b frontend project-init
* angular routing
* scss as style
2020-12-22 07:57:00 +01:00
9cdae30f7e Fix stupid accident dataset (remove duplicates?!?!) 2020-12-22 02:26:58 +01:00
04c45e0b7a Update README.md 2020-12-21 21:20:51 +00:00
108 changed files with 33670 additions and 175 deletions

View File

@ -8,5 +8,6 @@ Teammitglieder:
http://marcel.schwarz.gitlab.io/geovisualisierung/project-1/ http://marcel.schwarz.gitlab.io/geovisualisierung/project-1/
## Projekt 2 ## Projekt 2
http://marcel.schwarz.gitlab.io/geovisualisierung/project-2/ http://marcel.schwarz.gitlab.io/geovisualisierung/project-2/
## Projekt 3
https://it-schwarz.net

View File

@ -25,9 +25,9 @@ docker build -t geovis-backend .
``` ```
After the build make sure you are in the same directory as "bike-data.db" resides, if so, run After the build make sure you are in the same directory as "bike-data.db" resides, if so, run
```shell ```shell
docker run -v $(pwd):/app -p 8080:80 --restart always -d test docker run -v $(pwd):/app -p 8080:80 --restart always -d geovis-backend
``` ```
Note: `$(pwd)` puts the current directory in the command, if you are on Windows, you can use WSL or provide the full path by typing it out. Note: `$(pwd)` puts the current directory in the command, if you are on Windows, you can use WSL or provide the full path by typing it out.
To stop just shut down the container. To stop just shut down the container.

View File

@ -13,9 +13,7 @@ app = FastAPI(
origins = [ origins = [
"http://it-schwarz.net", "http://it-schwarz.net",
"https://it-schwarz.net", "https://it-schwarz.net"
"http://localhost",
"http://localhost:4200",
] ]
app.add_middleware( app.add_middleware(

View File

@ -37,36 +37,50 @@ def get_dashboard(station_id):
JOIN bike_points b ON u.start_station_id = b.id_num JOIN bike_points b ON u.start_station_id = b.id_num
JOIN dashboard d ON u.start_station_id = d.id JOIN dashboard d ON u.start_station_id = d.id
WHERE u.start_station_id = ?""" WHERE u.start_station_id = ?"""
return get_db_connection().execute(query, (station_id,)).fetchall() return get_db_connection().execute(query, (station_id,)).fetchone()
def get_dashboard_to(station_id, start_date, end_date): def get_dashboard_to(station_id, start_date, end_date):
query = """ query = """
SELECT SELECT
u.start_station_name AS startStationName, topPoints.*,
u.end_station_name AS endStationName, b.lat AS stationLat,
count(*) AS number, b.lon AS stationLon
round(avg(u.duration)) AS avgDuration FROM (
FROM usage_stats u SELECT
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ? u.end_station_name AS stationName,
GROUP BY u.end_station_name u.end_station_id AS stationId,
ORDER BY number DESC count(*) AS number,
LIMIT 3""" round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.end_station_name
ORDER BY number DESC
LIMIT 3
) as topPoints
JOIN bike_points b ON b.id_num = topPoints.stationId"""
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
def get_dashboard_from(station_id, start_date, end_date): def get_dashboard_from(station_id, start_date, end_date):
query = """ query = """
SELECT SELECT
u.start_station_name AS startStationName, topPoints.*,
u.end_station_name AS endStationName, b.lat AS stationLat,
count(*) AS number, b.lon AS stationLon
round(avg(u.duration)) AS avgDuration FROM (
FROM usage_stats u SELECT
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ? u.start_station_name AS stationName,
GROUP BY u.start_station_name u.start_station_id AS stationId,
ORDER BY number DESC count(*) AS number,
LIMIT 3""" round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.start_station_name
ORDER BY number DESC
LIMIT 3
) as topPoints
JOIN bike_points b ON b.id_num = topPoints.stationId"""
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall() return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()

View File

@ -2,8 +2,6 @@ import csv
import json import json
import logging import logging
import sqlite3 import sqlite3
import psycopg2
import psycopg2.extras
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@ -61,34 +59,34 @@ def get_online_files_list(subdir_filter=None, file_extension_filter=None):
def init_database(): def init_database():
LOG.info("Try to create tables") LOG.info("Try to create tables")
conn = get_conn() conn = sqlite3.connect(DB_NAME, timeout=300)
cursor = conn.cursor() conn.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
cursor.execute("""CREATE TABLE IF NOT EXISTS usage_stats( rental_id INTEGER PRIMARY KEY,
rental_id BIGINT PRIMARY KEY, duration INTEGER,
duration BIGINT, bike_id INTEGER,
bike_id BIGINT, end_date INTEGER,
end_date TIMESTAMP, end_station_id INTEGER,
end_station_id BIGINT,
end_station_name TEXT, end_station_name TEXT,
start_date TIMESTAMP, start_date INTEGER,
start_station_id BIGINT, start_station_id INTEGER,
start_station_name TEXT start_station_name TEXT
)""") )""")
cursor.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)") conn.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
cursor.execute("""CREATE TABLE IF NOT EXISTS bike_points( conn.execute("""CREATE TABLE IF NOT EXISTS bike_points(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
common_name TEXT, common_name TEXT,
lat REAL, lat REAL,
lon REAL, lon REAL,
id_num INTEGER id_num INTEGER
)""") )""")
cursor.execute("""CREATE TABLE IF NOT EXISTS accidents( conn.execute("""CREATE TABLE IF NOT EXISTS accidents(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
lat REAL, lat REAL,
lon REAL, lon REAL,
location TEXT, location TEXT,
date TIMESTAMP, date TEXT,
severity TEXT severity TEXT,
UNIQUE (lat, lon, date)
)""") )""")
conn.commit() conn.commit()
conn.close() conn.close()
@ -134,23 +132,20 @@ def create_dashboard_table():
def import_bikepoints(): def import_bikepoints():
LOG.info("Importing bikepoints") LOG.info("Importing bikepoints")
conn = get_conn() conn = sqlite3.connect(DB_NAME, timeout=300)
cursor = conn.cursor()
points = json.loads(requests.get("https://api.tfl.gov.uk/BikePoint").text) points = json.loads(requests.get("https://api.tfl.gov.uk/BikePoint").text)
points = list(map(lambda p: (p['id'], p['commonName'], p['lat'], p['lon'], int(p['id'][11:])), points)) points = list(map(lambda p: (p['id'], p['commonName'], p['lat'], p['lon'], int(p['id'][11:])), points))
LOG.info(f"Writing {len(points)} bikepoints to DB") LOG.info(f"Writing {len(points)} bikepoints to DB")
cursor.executemany("INSERT INTO bike_points VALUES (%s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", points) conn.executemany("INSERT OR IGNORE INTO bike_points VALUES (?, ?, ?, ?, ?)", points)
conn.commit() conn.commit()
conn.close() conn.close()
LOG.info("Bikepoints imported") LOG.info("Bikepoints imported")
def import_accidents(year): def import_accidents(year):
LOG.info("Importing accidents") LOG.info(f"Importing accidents for year {year}")
conn = get_conn() conn = sqlite3.connect(DB_NAME, timeout=300)
cursor = conn.cursor()
def filter_pedal_cycles(accident): def filter_pedal_cycles(accident):
for vehicle in accident['vehicles']: for vehicle in accident['vehicles']:
@ -161,22 +156,22 @@ def import_accidents(year):
accidents = requests.get(f"https://api.tfl.gov.uk/AccidentStats/{year}").text accidents = requests.get(f"https://api.tfl.gov.uk/AccidentStats/{year}").text
accidents = json.loads(accidents) accidents = json.loads(accidents)
accidents = list(filter(filter_pedal_cycles, accidents)) accidents = list(filter(filter_pedal_cycles, accidents))
accidents = list(map(lambda a: (a['id'], a['lat'], a['lon'], a['location'], a['date'], a['severity']), accidents)) accidents = list(map(lambda a: (a['lat'], a['lon'], a['location'], a['date'], a['severity']), accidents))
LOG.info(f"Writing {len(accidents)} bike accidents to DB") LOG.info(f"Writing {len(accidents)} bike accidents to DB")
cursor.executemany("INSERT INTO accidents VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", accidents) conn.executemany("""INSERT OR IGNORE INTO
accidents(lat, lon, location, date, severity)
VALUES (?, ?, ?, ?, ?)""", accidents)
conn.commit() conn.commit()
conn.close() conn.close()
LOG.info("Accidents importet") LOG.info(f"Accidents imported for year {year}")
def import_usage_stats_file(export_file: ApiExportFile): def import_usage_stats_file(export_file: ApiExportFile):
conn = get_conn() conn = sqlite3.connect(DB_NAME, timeout=300)
cursor = conn.cursor()
cursor.execute("SELECT * FROM read_files WHERE etag = %s", (export_file.etag,)) rows = conn.execute("SELECT * FROM read_files WHERE etag LIKE ?", (export_file.etag,)).fetchall()
if len(cursor.fetchall()) != 0: if len(rows) != 0:
LOG.warning(f"Skipping import of {export_file.path}") LOG.warning(f"Skipping import of {export_file.path}")
return return
@ -196,13 +191,13 @@ def import_usage_stats_file(export_file: ApiExportFile):
# Bike Id # Bike Id
int(entry[2] or "-1"), int(entry[2] or "-1"),
# End Date # End Date
datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M") if entry[3] else None, int(datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[3] else -1,
# EndStation Id # EndStation Id
int(entry[4] or "-1"), int(entry[4] or "-1"),
# EndStation Name # EndStation Name
entry[5].strip(), entry[5].strip(),
# Start Date # Start Date
datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M") if entry[6] else None, int(datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[6] else -1,
# StartStation Id # StartStation Id
int(entry[7]), int(entry[7]),
# StartStation Name # StartStation Name
@ -215,45 +210,36 @@ def import_usage_stats_file(export_file: ApiExportFile):
LOG.error(f"Key Error {e} on line {entry}") LOG.error(f"Key Error {e} on line {entry}")
return return
LOG.info(f"Writing {len(mapped)} entries to DB") LOG.info(f"Writing {len(mapped)} entries to DB")
psycopg2.extras.execute_values(cursor, "INSERT INTO usage_stats VALUES %s ON CONFLICT DO NOTHING ", mapped, page_size=1_000_000) conn.executemany("INSERT OR IGNORE INTO usage_stats VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", mapped)
cursor.execute("INSERT INTO read_files VALUES (%s, %s) ON CONFLICT DO NOTHING", (export_file.path, export_file.etag)) conn.execute("INSERT OR IGNORE INTO read_files VALUES (?, ?)", (export_file.path, export_file.etag))
conn.commit() conn.commit()
conn.close()
LOG.info(f"Finished import of {export_file.path}") LOG.info(f"Finished import of {export_file.path}")
def get_conn():
return psycopg2.connect(
host="localhost",
database="postgres",
user="postgres",
password="supersecure"
)
def main(): def main():
# General DB init # General DB init
init_database() init_database()
import_accidents(2019)
import_bikepoints()
# count_pre = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0] count_pre = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
#
# Download and import opendata from S3 bucket # Download and import opendata from S3 bucket
all_files = get_online_files_list(subdir_filter="usage-stats", file_extension_filter=".csv") all_files = get_online_files_list(subdir_filter="usage-stats", file_extension_filter=".csv")
for file in all_files: for file in all_files:
import_usage_stats_file(file) import_usage_stats_file(file)
#
# count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0] count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
#
# # Create search-index for faster querying # Create search-index for faster querying
# create_indexes() create_indexes()
# # Import Bikepoints # Import Bikepoints
# import_bikepoints() import_bikepoints()
# # Import bike accidents # Import bike accidents
# import_accidents(2019) for year in range(2005, 2020):
# import_accidents(year)
# if count_after - count_pre > 0:
# create_dashboard_table() if count_after - count_pre > 0:
create_dashboard_table()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,4 +1,5 @@
import logging import logging
from enum import Enum
from typing import List from typing import List
from fastapi import APIRouter from fastapi import APIRouter
@ -10,10 +11,16 @@ router = APIRouter(prefix="/accidents", tags=["accidents", "local"])
LOG = logging.getLogger() LOG = logging.getLogger()
class Severity(str, Enum):
slight = "Slight"
serious = "Serious"
fatal = "Fatal"
class Accident(BaseModel): class Accident(BaseModel):
lat: float lat: float
lon: float lon: float
severity: str severity: Severity
@router.get( @router.get(

View File

@ -1,4 +1,4 @@
import datetime from datetime import date, datetime, time, timedelta
from typing import Optional, List from typing import Optional, List
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
@ -9,7 +9,7 @@ import api_database
router = APIRouter(prefix="/dashboard/{station_id}", tags=["dashboard", "local"]) router = APIRouter(prefix="/dashboard/{station_id}", tags=["dashboard", "local"])
def validate_daterange(start_date: datetime.date, end_date: datetime.date): def validate_daterange(start_date: date, end_date: date):
days_requested = (end_date - start_date).days days_requested = (end_date - start_date).days
if days_requested < 0: if days_requested < 0:
raise HTTPException(status_code=400, detail="Requested date-range is negative") raise HTTPException(status_code=400, detail="Requested date-range is negative")
@ -20,30 +20,32 @@ class StationDashboard(BaseModel):
commonName: Optional[str] commonName: Optional[str]
lat: Optional[float] lat: Optional[float]
lon: Optional[float] lon: Optional[float]
maxEndDate: Optional[datetime.date] maxEndDate: Optional[date]
maxStartDate: Optional[datetime.date] maxStartDate: Optional[date]
@router.get("/", response_model=StationDashboard) @router.get("/", response_model=StationDashboard)
def get_general_dashboard(station_id: int): def get_general_dashboard(station_id: int):
return api_database.get_dashboard(station_id)[0] return api_database.get_dashboard(station_id) or {}
class StationDashboardTopStationsEntry(BaseModel): class StationDashboardTopStationsEntry(BaseModel):
startStationName: str stationName: str
endStationName: str stationId: int
stationLat: float
stationLon: float
number: int number: int
avgDuration: int avgDuration: int
@router.get("/to", response_model=List[StationDashboardTopStationsEntry]) @router.get("/to", response_model=List[StationDashboardTopStationsEntry])
def get_to_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): def get_to_dashboard_for_station(station_id: int, start_date: date, end_date: date):
validate_daterange(start_date, end_date) validate_daterange(start_date, end_date)
return api_database.get_dashboard_to(station_id, start_date, end_date) return api_database.get_dashboard_to(station_id, start_date, end_date)
@router.get("/from", response_model=List[StationDashboardTopStationsEntry]) @router.get("/from", response_model=List[StationDashboardTopStationsEntry])
def get_from_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): def get_from_dashboard_for_station(station_id: int, start_date: date, end_date: date):
validate_daterange(start_date, end_date) validate_daterange(start_date, end_date)
return api_database.get_dashboard_from(station_id, start_date, end_date) return api_database.get_dashboard_from(station_id, start_date, end_date)
@ -54,9 +56,21 @@ class StationDashboardDurationGroup(BaseModel):
@router.get("/duration", response_model=List[StationDashboardDurationGroup]) @router.get("/duration", response_model=List[StationDashboardDurationGroup])
def get_duration_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): def get_duration_dashboard_for_station(station_id: int, start_date: date, end_date: date):
validate_daterange(start_date, end_date) validate_daterange(start_date, end_date)
return api_database.get_dashboard_duration(station_id, start_date, end_date) db_data = api_database.get_dashboard_duration(station_id, start_date, end_date)
ret_val = []
for group in ['0-5', '5-15', '15-30', '30-45', '45+']:
curr_minute_group = list(filter(lambda x: x['minutesGroup'] == group, db_data))
if curr_minute_group:
item = curr_minute_group.pop()
ret_val.append({
'number': item['number'],
'minutesGroup': item['minutesGroup']
})
else:
ret_val.append({'number': 0, 'minutesGroup': group})
return ret_val
class StationDashboardTimeGroup(BaseModel): class StationDashboardTimeGroup(BaseModel):
@ -66,6 +80,18 @@ class StationDashboardTimeGroup(BaseModel):
@router.get("/time", response_model=List[StationDashboardTimeGroup]) @router.get("/time", response_model=List[StationDashboardTimeGroup])
def get_time_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date): def get_time_dashboard_for_station(station_id: int, start_date: date, end_date: date):
validate_daterange(start_date, end_date) validate_daterange(start_date, end_date)
return api_database.get_dashboard_time(station_id, start_date, end_date) db_data = api_database.get_dashboard_time(station_id, start_date, end_date)
ret_val = []
init_date = datetime.combine(date.today(), time(0, 0))
for i in range(144):
curr_interval = (init_date + timedelta(minutes=10 * i)).strftime("%H:%M")
search_interval = list(filter(lambda x: x['timeFrame'] == curr_interval, db_data))
if search_interval:
ret_val.append(search_interval.pop())
else:
ret_val.append({'timeFrame': curr_interval, 'number': 0, 'avgDuration': 0})
return ret_val

View File

@ -1,75 +0,0 @@
-- Tables
CREATE TABLE IF NOT EXISTS usage_stats(
rental_id BIGINT PRIMARY KEY,
duration BIGINT,
bike_id BIGINT,
end_date TIMESTAMP,
end_station_id BIGINT,
end_station_name TEXT,
start_date TIMESTAMP,
start_station_id BIGINT,
start_station_name TEXT
);
INSERT INTO usage_stats
VALUES (40346508, 360, 12019, TO_TIMESTAMP(1420326360), 424, 'Ebury Bridge, Pimlico', TO_TIMESTAMP(1420326000), 368, 'Harriet Street, Knightsbridge')
ON CONFLICT DO NOTHING;
SELECT TO_TIMESTAMP(1420326360);
CREATE TABLE IF NOT EXISTS read_files(
file_path TEXT,
etag TEXT PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS bike_points(
id TEXT PRIMARY KEY,
common_name TEXT,
lat REAL,
lon REAL,
id_num BIGINT
);
CREATE TABLE IF NOT EXISTS accidents(
id BIGINT PRIMARY KEY,
lat REAL,
lon REAL,
location TEXT,
date TEXT,
severity TEXT
);
-- indicies
CREATE INDEX IF NOT EXISTS idx_station_start_and_end_date ON usage_stats (start_station_id, start_date, end_date);
SELECT COUNT(*) FROM usage_stats;
SELECT
min(u.start_station_name) AS startStationName,
u.end_station_name AS endStationName,
count(*) AS number,
round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.start_station_id = 512 AND u.start_date::DATE >= '2010-01-01'::DATE AND u.start_date::DATE <= '2022-01-15'::DATE
GROUP BY u.end_station_name
ORDER BY number DESC
LIMIT 3;
SELECT
MIN(b.id_num) as id,
MIN(b.common_name) AS commonName,
MIN(b.lat),
MIN(b.lon),
max(u.start_date) AS maxEndDate,
min(u.start_date) AS maxStartDate
FROM usage_stats u
JOIN bike_points b ON u.start_station_id = b.id_num
WHERE u.start_station_id = 306

View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

48
projects/project-3/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
**/*.css
**/*.css.map
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
**/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,27 @@
# Frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,144 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/frontend",
"allowedCommonJsDependencies": [
"apexcharts"
],
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss",
"src/theme.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "frontend:build"
},
"configurations": {
"production": {
"browserTarget": "frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images/",
"output": "./assets"
}
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss",
"src/theme.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "frontend:serve"
},
"configurations": {
"production": {
"devServerTarget": "frontend:serve:production"
}
}
}
}
}
},
"defaultProject": "frontend",
"cli": {
"analytics": false
}
}

View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('frontend app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/frontend'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

30056
projects/project-3/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.2.0",
"@angular/cdk": "^10.2.7",
"@angular/common": "~10.2.0",
"@angular/compiler": "~10.2.0",
"@angular/core": "~10.2.0",
"@angular/flex-layout": "^10.0.0-beta.32",
"@angular/forms": "~10.2.0",
"@angular/material": "^10.2.7",
"@angular/platform-browser": "~10.2.0",
"@angular/platform-browser-dynamic": "~10.2.0",
"@angular/router": "~10.2.0",
"apexcharts": "^3.23.0",
"bootstrap": "^4.5.3",
"jquery": "^3.5.1",
"leaflet": "~1.3.1",
"leaflet.heat": "^0.2.0",
"leaflet.markercluster": "^1.4.1",
"ng-apexcharts": "^1.5.6",
"rxjs": "~6.6.0",
"seconds-to-human-time": "^1.0.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.0",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
}
}

View File

@ -0,0 +1,17 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MapComponent} from './map/map.component';
import {DashboardComponent} from './dashboard/dashboard.component';
const routes: Routes = [
{path: '', redirectTo: 'map', pathMatch: 'full'},
{path: 'map', component: MapComponent},
{path: 'dashboard/:id', component: DashboardComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View File

@ -0,0 +1,3 @@
<app-toolbar></app-toolbar>
<router-outlet></router-outlet>
<app-footer></app-footer>

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'frontend'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
});
});

View File

@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {Title} from '@angular/platform-browser';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public constructor(private title: Title) {
this.title.setTitle('Bike Stations in London');
}
}

View File

@ -0,0 +1,90 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {MapComponent} from './map/map.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatToolbarModule} from '@angular/material/toolbar';
import {FlexLayoutModule} from '@angular/flex-layout';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {HttpClientModule} from '@angular/common/http';
import {NgApexchartsModule} from 'ng-apexcharts';
import {DashboardComponent} from './dashboard/dashboard.component';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatCardModule} from '@angular/material/card';
import {MatMenuModule} from '@angular/material/menu';
import {LayoutModule} from '@angular/cdk/layout';
import {PopUpComponent} from './map/pop-up/pop-up.component';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatNativeDateModule} from '@angular/material/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatInputModule} from '@angular/material/input';
import {MatTableModule} from '@angular/material/table';
import {AutoRefreshComponent} from './map/auto-refresh/auto-refresh.component';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatTooltipModule} from '@angular/material/tooltip';
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import { TableComponent } from './dashboard/table/table.component';
import { RentDurationChartComponent } from './dashboard/rent-duration-chart/rent-duration-chart.component';
import { RentTimeChartComponent } from './dashboard/rent-time-chart/rent-time-chart.component';
import { UserInputComponent } from './dashboard/user-input/user-input.component';
import { MiniMapComponent } from './dashboard/mini-map/mini-map.component';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { MapInteractionComponent } from './toolbar/map-interaction/map-interaction.component';
import { DashboardInteractionComponent } from './toolbar/dashboard-interaction/dashboard-interaction.component';
import { FooterComponent } from './footer/footer.component';
@NgModule({
declarations: [
AppComponent,
MapComponent,
DashboardComponent,
PopUpComponent,
AutoRefreshComponent,
TableComponent,
RentDurationChartComponent,
RentTimeChartComponent,
UserInputComponent,
MiniMapComponent,
ToolbarComponent,
MapInteractionComponent,
DashboardInteractionComponent,
FooterComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
FlexLayoutModule,
HttpClientModule,
NgApexchartsModule,
MatGridListModule,
MatCardModule,
MatMenuModule,
LayoutModule,
MatSidenavModule,
MatDatepickerModule,
MatFormFieldModule,
MatNativeDateModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatTableModule,
MatSlideToggleModule,
MatCheckboxModule,
MatTooltipModule,
MatProgressSpinnerModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

@ -0,0 +1,24 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<div class="px-5 py-3" style="background: #2f2f2f">
<div>
<div class="row mb-3">
<app-user-input
(startEndDate)="onSubmit($event)"
class="col-xl-5 col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
</app-user-input>
<app-mini-map class="col-xl-7 col-lg-6 col-md-12"></app-mini-map>
</div>
<div class="mb-3">
<app-table></app-table>
</div>
<div class="row mb-3">
<app-rent-duration-chart class="col"></app-rent-duration-chart>
</div>
<div class="row mb-3">
<app-rent-time-chart class="col"></app-rent-time-chart>
</div>
</div>
</div>

View File

@ -0,0 +1,40 @@
import { LayoutModule } from '@angular/cdk/layout';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardComponent],
imports: [
NoopAnimationsModule,
LayoutModule,
MatButtonModule,
MatCardModule,
MatGridListModule,
MatIconModule,
MatMenuModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
import {IDashboardCommonBikePoint} from '../service/domain/dashboard-common-bike-point';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
import {TableComponent} from './table/table.component';
import {RentDurationChartComponent} from './rent-duration-chart/rent-duration-chart.component';
import {RentTimeChartComponent} from './rent-time-chart/rent-time-chart.component';
import {StartEndDate} from './user-input/user-input.component';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartHeight = 460;
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
})
export class DashboardComponent implements OnInit {
@ViewChild(TableComponent) table: TableComponent;
@ViewChild(RentDurationChartComponent) durationChart: RentDurationChartComponent;
@ViewChild(RentTimeChartComponent) timeChart: RentTimeChartComponent;
constructor() {
}
ngOnInit(): void {
}
async onSubmit(startEndDate: StartEndDate): Promise<any> {
await this.table.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
await this.durationChart.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
await this.timeChart.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
}
}

View File

@ -0,0 +1,3 @@
<mat-card class="p-0">
<div id="minimap" style="height: 30rem"></div>
</mat-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MiniMapComponent } from './mini-map.component';
describe('MiniMapComponent', () => {
let component: MiniMapComponent;
let fixture: ComponentFixture<MiniMapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MiniMapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MiniMapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {MapService} from '../../service/map.service';
@Component({
selector: 'app-mini-map',
templateUrl: './mini-map.component.html',
styleUrls: ['./mini-map.component.scss']
})
export class MiniMapComponent implements OnInit {
bikePoint: IDashboardCommonBikePoint;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
private map: MapService
) { }
ngOnInit(): void {
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.initMap();
});
});
}
initMap(): void {
this.map.initDashboardMap(this.bikePoint.lat, this.bikePoint.lon, 17);
this.map.drawDashboardStationMarker(this.bikePoint);
}
}

View File

@ -0,0 +1,27 @@
<mat-card>
<mat-card-header>
<mat-card-title>Rental Duration</mat-card-title>
<mat-card-subtitle>
This chart shows the rent duration based on the currently selected station.
The time it takes for a rent which has the current station as origin is displayed here.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="!isLoading" class="station-dashboard-borrow-duration">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"></apx-chart>
</div>
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RentDurationChartComponent } from './rent-duration-chart.component';
describe('RentDurationChartComponent', () => {
let component: RentDurationChartComponent;
let fixture: ComponentFixture<RentDurationChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RentDurationChartComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RentDurationChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,159 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis,
ChartComponent
} from 'ng-apexcharts';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis;
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartType = 'duration';
@Component({
selector: 'app-rent-duration-chart',
templateUrl: './rent-duration-chart.component.html',
styleUrls: ['./rent-duration-chart.component.scss']
})
export class RentDurationChartComponent implements OnInit {
@ViewChild(ChartComponent) chart: ChartComponent;
chartOptions: Partial<ChartOptions>;
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoading: boolean;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
) {
this.chartOptions = {
series: [],
chart: {
type: 'bar'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.isLoading = true;
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initChart().catch(error => console.log(error));
});
});
}
async initChart(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
this.isLoading = false;
this.chartOptions = {
series: [
{
name: 'amount of drives',
data: source.map(value => value.number)
}
],
chart: {
type: 'bar',
height: '460',
toolbar: {
show: false
}
},
colors: ['#017bfe'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
endingShape: 'flat'
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
title: {
text: 'average rental duration'
},
categories: source.map(value => value.minutesGroup),
labels: {
formatter: value => {
return value + ' min';
}
}
},
yaxis: {
title: {
text: 'amount of drives'
}
},
noData: {
text: 'loading'
},
fill: {
opacity: 1
}
};
});
}
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoading = true;
this.service.fetchDashboardStationCharts(
this.bikePoint.id,
actualStartDate,
actualEndDate,
chartType
).then(source => {
this.isLoading = false;
setTimeout(() => {
this.chart.updateSeries([{
data: source.map(value => value.number)
}]);
}, 1000);
});
}
}

View File

@ -0,0 +1,30 @@
<mat-card>
<mat-card-header>
<mat-card-title>Rental Time</mat-card-title>
<mat-card-subtitle>
This chart shows the workload of the currently selected station in relation
of the time of the day. It is visualized at which time of the day a journey begins or ends (blue).
In addition, the average rental duration of the trips is displayed at the given time (green).
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
</div>
<div *ngIf="!isLoading" class="station-dashboard-borrow-time">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis">
</apx-chart>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RentTimeChartComponent } from './rent-time-chart.component';
describe('RentTimeChartComponent', () => {
let component: RentTimeChartComponent;
let fixture: ComponentFixture<RentTimeChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RentTimeChartComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RentTimeChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,175 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis,
ChartComponent
} from 'ng-apexcharts';
import {DashboardService} from '../../service/dashboard.service';
import {ActivatedRoute} from '@angular/router';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartType = 'time';
@Component({
selector: 'app-rent-time-chart',
templateUrl: './rent-time-chart.component.html',
styleUrls: ['./rent-time-chart.component.scss']
})
export class RentTimeChartComponent implements OnInit {
@ViewChild(ChartComponent) chart: ChartComponent;
chartOptions: Partial<ChartOptions>;
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoading: boolean;
constructor(
private route: ActivatedRoute,
private service: DashboardService
) {
this.chartOptions = {
series: [],
chart: {
type: 'line'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.isLoading = true;
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initChart().catch(error => console.log(error));
});
});
}
async initChart(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
this.isLoading = false;
this.chartOptions = {
series: [
{
name: 'amount of drives',
type: 'bar',
data: source.map(value => value.number)
},
{
name: 'average rental duration',
type: 'line',
data: source.map(value => Math.round(value.avgDuration / 60))
}
],
tooltip: {
enabled: true,
shared: true,
x: {
show: true
}
},
chart: {
toolbar: {
show: false
},
type: 'line',
height: '495',
zoom: {
enabled: true,
}
},
colors: ['#017bfe', '#51ca49'],
dataLabels: {
enabled: false,
},
stroke: {
curve: 'straight'
},
legend: {
show: true,
},
xaxis: {
title: {
text: 'time of the day'
},
categories: source.map(value => value.timeFrame),
tickAmount: 24,
tickPlacement: 'between'
},
yaxis: [{
title: {
text: 'amount of drives',
},
}, {
opposite: true,
title: {
text: 'average rental duration'
},
labels: {
formatter: (val: number): string => {
return val + ' min';
}
}
}],
fill: {
opacity: 1
}
};
});
}
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoading = true;
this.service.fetchDashboardStationCharts(
this.bikePoint.id,
actualStartDate,
actualEndDate,
chartType
).then(source => {
this.isLoading = false;
setTimeout(() => {
this.chart.updateSeries([{
data: source.map(value => value.number)
}, {
data: source.map(value => Math.round(value.avgDuration / 60))
}]);
}, 1000);
});
}
}

View File

@ -0,0 +1,110 @@
<div class="row">
<div class="col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
<mat-card>
<mat-card-header>
<mat-card-title>Top-3 rental destination</mat-card-title>
<mat-card-subtitle>
This table shows the top-3 destinations of rentals from this station by number of drives.
The Station can be sent to the map with the checkbox.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table [dataSource]="stationToSource" class="mat-elevation-z0 w-100" mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" class="p-3" mat-cell>
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
(click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
[disabled]="isCheckBoxDisable(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="endStationName">
<th *matHeaderCellDef mat-header-cell>Destination</th>
<td *matCellDef="let element" mat-cell>
<a [routerLink]="['/dashboard/', element.stationId]">{{element.stationName}}</a>
</td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell>Count</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell>Average duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell>Icon</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsTo" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsTo;" mat-row></tr>
</table>
<div *ngIf="isLoadingToSource" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="col-lg-6 col-md-12">
<mat-card>
<mat-card-header>
<mat-card-title>Top-3 rental origin</mat-card-title>
<mat-card-subtitle>
This table shows the top-3 origins of rentals to this station by number of drives.
The Station can be sent to the map with the checkbox.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table [dataSource]="stationFromSource" class="mat-elevation-z0 w-100" mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" class="p-3" mat-cell>
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
(click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
[disabled]="isCheckBoxDisable(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="startStationName">
<th *matHeaderCellDef mat-header-cell>Origin</th>
<td *matCellDef="let element" mat-cell>
<a [routerLink]="['/dashboard/', element.stationId]"> {{element.stationName}}</a>
</td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell>Count</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell>Average duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell>Icon</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsFrom" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsFrom;" mat-row></tr>
</table>
<div *ngIf="isLoadingFromSource" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,17 @@
img {
width: 60px;
}
a {
color: black;
text-decoration: underline;
}
.mat-checkbox-layout label {
margin: 0 !important;
}
.mat-cell, .mat-header-cell {
padding-left: 8px;
padding-right: 8px;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TableComponent } from './table.component';
describe('TableComponent', () => {
let component: TableComponent;
let fixture: ComponentFixture<TableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TableComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,179 @@
import {Component, OnInit} from '@angular/core';
import {MatTableDataSource} from '@angular/material/table';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {SelectionModel} from '@angular/cdk/collections';
import {MatCheckboxChange} from '@angular/material/checkbox';
import stht from 'seconds-to-human-time';
import {MapService} from '../../service/map.service';
import {DashboardService} from '../../service/dashboard.service';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss']
})
export class TableComponent implements OnInit {
displayedColumnsTo: string[] = ['select', 'endStationName', 'number', 'avgDuration', 'marker'];
displayedColumnsFrom: string[] = ['select', 'startStationName', 'number', 'avgDuration', 'marker'];
stationToSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableToSource: any[];
stationFromSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableFromSource: any[];
selectionModel = new SelectionModel<IDashboardCommonBikePoint>(true, []);
colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoadingToSource = true;
isLoadingFromSource = true;
constructor(
private route: ActivatedRoute,
private map: MapService,
private service: DashboardService
) {
}
ngOnInit(): void {
this.route.params.subscribe(params => {
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initTable();
});
});
}
initTable(): void {
this.selectionModel.clear();
this.map.removeOverlayOnMiniMap();
const initDate = this.maxEndDate.toISOString().substring(0, 10);
this.loadData(initDate, initDate);
}
onSubmit(actualStartDate: string, actualEndDate: string): void {
this.resetTableSourcesToDisplaySpinner();
this.selectionModel.clear();
this.map.removeOverlayOnMiniMap();
this.loadData(actualStartDate, actualEndDate);
}
resetTableSourcesToDisplaySpinner(): void {
this.isLoadingToSource = true;
this.isLoadingFromSource = true;
this.stationToSource = null;
this.stationFromSource = null;
this.iterableToSource = [];
this.iterableFromSource = [];
}
async loadData(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoadingToSource = true;
this.isLoadingFromSource = true;
const [stationTo, stationFrom] = await Promise.all([
this.service.fetchDashboardStationTo(this.bikePoint.id, actualStartDate, actualEndDate),
this.service.fetchDashboardStationFrom(this.bikePoint.id, actualStartDate, actualEndDate)
]);
this.isLoadingToSource = false;
this.isLoadingFromSource = false;
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
this.stationToSource = this.setBikePointColorToSource(stationTo);
this.iterableToSource = stationTo;
this.iterableToSource.forEach(bikePoint => bikePoint.polyLineColor = 'red');
this.stationFromSource = this.setBikePointColorFromSource(stationFrom);
this.iterableFromSource = stationFrom;
this.iterableFromSource.forEach(bikePoint => bikePoint.polyLineColor = 'green');
this.selectionModel.select(...this.iterableFromSource.filter(bikePoint => bikePoint.stationId === this.bikePoint.id));
this.selectionModel.select(...this.iterableToSource.filter(bikePoint => bikePoint.stationId === this.bikePoint.id));
}
public drawIconInTable(bikePoint: any): string {
return `../../assets/bike-point-${bikePoint.color}.png`;
}
humanizeAvgDuration(avgDuration: number): string {
return stht(avgDuration);
}
selectRow(selection: MatCheckboxChange, row): void {
let markerToDisplay = [];
this.iterableToSource.forEach(point => {
if (point.stationId === row.stationId) {
this.selectionModel.toggle(point);
}
});
this.iterableFromSource.forEach(point => {
if (point.stationId === row.stationId) {
this.selectionModel.toggle(point);
}
});
this.selectionModel.selected.forEach(point => {
markerToDisplay.push(point);
});
markerToDisplay = this.changePolyLineColorForDuplicateBikePoints(markerToDisplay);
this.map.drawTableStationMarker(markerToDisplay);
}
changePolyLineColorForDuplicateBikePoints(array: any[]): any[] {
const id = array.map(item => item.stationId);
const duplicates = id.filter((value, index) => id.indexOf(value) !== index);
duplicates.forEach(stationId => {
array.forEach(bikePoint => {
if (bikePoint.stationId === stationId) {
bikePoint.polyLineColor = 'blue';
}
});
});
return array;
}
setBikePointColorToSource(source): any {
for (const station of source) {
if (station.stationId === this.bikePoint.id) {
station.color = 'blue';
continue;
}
station.color = this.getRandomColor();
}
return source;
}
setBikePointColorFromSource(source): any {
for (const station of source) {
if (station.stationId === this.bikePoint.id) {
station.color = 'blue';
continue;
}
for (const to of this.iterableToSource) {
if (station.stationId === to.stationId) {
station.color = to.color;
break;
}
}
if (!station.color) {
station.color = this.getRandomColor();
}
}
return source;
}
getRandomColor(): string {
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
this.colors = this.colors.filter(c => c !== color);
return color;
}
isCheckBoxDisable(row): boolean {
return row.stationId === this.bikePoint.id;
}
}

View File

@ -0,0 +1,45 @@
<mat-card style="height: 30rem">
<mat-card-header>
<mat-card-title class="d-flex align-items-center">
<div class="header-image d-inline-flex" mat-card-avatar></div>
{{bikePoint?.commonName}}
</mat-card-title>
</mat-card-header>
<mat-card-content class="p-4 d-flex flex-column justify-content-center align-content-between">
<p>Select a range to analyze data</p>
<form [formGroup]="form" (submit)="onSubmit()">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Enter a range</mat-label>
<mat-date-range-input [max]="maxEndDate" [min]="maxStartDate" [rangePicker]="picker"
formGroupName="dateRange">
<input formControlName="start" matStartDate placeholder="Start date">
<input formControlName="end" matEndDate placeholder="End date">
</mat-date-range-input>
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<button (click)="onSubmit()" color="primary" type="submit" (keyup.enter)="onSubmit()" class="w-100" mat-raised-button>
reload
<mat-icon>cached</mat-icon>
</button>
</form>
<br/>
<div class="ml-n4" id="chart">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[subtitle]="chartOptions.subtitle"
[title]="chartOptions.title"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"
></apx-chart>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,4 @@
.header-image {
background-image: url('../../../assets/bike-point-blue.png');
background-size: cover;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserInputComponent } from './user-input.component';
describe('UserInputComponent', () => {
let component: UserInputComponent;
let fixture: ComponentFixture<UserInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserInputComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UserInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,223 @@
import {Component, EventEmitter, Injectable, OnInit, Output} from '@angular/core';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {FormBuilder, FormControl, FormGroup} from '@angular/forms';
import {IMapBikePoint} from '../../service/domain/map-bike-point';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
import {DateAdapter, MAT_DATE_FORMATS, NativeDateAdapter} from '@angular/material/core';
import {formatDate} from '@angular/common';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
export const PICK_FORMATS = {
parse: {dateInput: {month: 'short', year: 'numeric', day: 'numeric'}},
display: {
dateInput: 'input',
monthYearLabel: {year: 'numeric', month: 'numeric'},
dateA11yLabel: {year: 'numeric', month: 'numeric', day: 'numeric'},
monthYearA11yLabel: {year: 'numeric', month: 'long'}
}
};
@Injectable()
class PickDateAdapter extends NativeDateAdapter {
format(date: Date, displayFormat: Object): string {
if (displayFormat === 'input') {
return formatDate(date, 'dd.MM.yyyy', this.locale);
} else {
return date.toDateString();
}
}
}
export interface StartEndDate {
actualStartDate: Date;
actualEndDate: Date;
}
@Component({
selector: 'app-user-input',
templateUrl: './user-input.component.html',
styleUrls: ['./user-input.component.scss'],
providers: [
{provide: DateAdapter, useClass: PickDateAdapter},
{provide: MAT_DATE_FORMATS, useValue: PICK_FORMATS}
]
})
export class UserInputComponent implements OnInit {
@Output() startEndDate: EventEmitter<StartEndDate> = new EventEmitter<StartEndDate>();
chartOptions: Partial<ChartOptions>;
station: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
form: FormGroup;
bikePoint: IMapBikePoint;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
private fb: FormBuilder
) {
this.chartOptions = {
series: [],
chart: {
type: 'bar'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.form = this.fb.group({
dateRange: new FormGroup({
start: new FormControl(),
end: new FormControl()
})
});
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.station = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initInput().catch(error => console.log(error));
});
this.service.fetchBikePointForStatus(params.id).then(data => {
this.bikePoint = data;
const NbBlockedDocks = data.status.NbDocks - data.status.NbBikes - data.status.NbEmptyDocks;
this.chartOptions = {
subtitle: {
text: 'This chart visualizes the availability of the bikes',
offsetX: 20,
offsetY: 15,
style: {
fontSize: '15px'
}
},
series: [
{
name: 'Bikes',
data: [data.status.NbBikes]
},
{
name: 'Empty docks',
data: [data.status.NbEmptyDocks]
},
{
name: 'Blocked docks',
data: [NbBlockedDocks]
}
],
colors: ['#51ca49', '#8f8e8e', '#f00'],
chart: {
type: 'bar',
height: 180,
stacked: true,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: true,
dataLabels: {
position: 'center'
}
}
},
dataLabels: {
enabled: true,
style: {
fontSize: '20px',
colors: ['#fff']
}
},
stroke: {
show: false
},
xaxis: {
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
title: {
text: undefined
},
axisBorder: {
show: false
},
min: 0,
max: data.status.NbDocks
},
tooltip: {
enabled: false,
},
fill: {
opacity: 1
},
legend: {
position: 'bottom',
horizontalAlign: 'right',
fontSize: '14px'
}
};
});
});
}
async initInput(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
this.form.get('dateRange').get('start').setValue(initDate);
this.form.get('dateRange').get('end').setValue(initDate);
}
async onSubmit(): Promise<any> {
this.startEndDate.emit({
actualStartDate: this.form.get('dateRange').value.start,
actualEndDate: this.form.get('dateRange').value.end
});
}
}

View File

@ -0,0 +1,5 @@
<div class="footer d-flex justify-content-center align-items-center">
<div class="copyright">
<span>&copy; Tim Herbst & Marcel Schwarz</span>
</div>
</div>

View File

@ -0,0 +1,8 @@
.footer {
height: 2vh;
background: #2f2f2f;
.copyright {
color: white;
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,5 @@
<div>
<mat-slide-toggle [(ngModel)]="isFlagActive" (ngModelChange)="onChange($event)" color="warn">
auto refresh
</mat-slide-toggle>
</div>

View File

@ -0,0 +1,4 @@
mat-slide-toggle {
margin-right: 1em;
font-size: 15px;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AutoRefreshComponent } from './auto-refresh.component';
describe('AutoRefreshComponent', () => {
let component: AutoRefreshComponent;
let fixture: ComponentFixture<AutoRefreshComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AutoRefreshComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AutoRefreshComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {MapService} from '../../service/map.service';
import * as internal from 'events';
@Component({
selector: 'app-auto-refresh',
templateUrl: './auto-refresh.component.html',
styleUrls: ['./auto-refresh.component.scss']
})
export class AutoRefreshComponent implements OnInit, OnDestroy {
isFlagActive: boolean;
interval: internal;
constructor(private map: MapService) {
const storageFlag = JSON.parse(sessionStorage.getItem('auto-refresh'));
if (storageFlag) {
this.isFlagActive = storageFlag;
} else {
this.isFlagActive = false;
}
}
ngOnInit(): void {
this.interval = setInterval(() => {
if (this.isFlagActive) {
this.map.autoRefresh().catch(error => console.log(error));
}
}, 10000);
}
ngOnDestroy(): void {
clearInterval(this.interval);
}
onChange(flag: boolean): void {
sessionStorage.setItem('auto-refresh', JSON.stringify(flag));
}
}

View File

@ -0,0 +1,7 @@
<div class="map-container" fxLayout="row">
<div class="map-frame" fxFill>
<div id="map"></div>
</div>
</div>

View File

@ -0,0 +1,3 @@
#map {
height: 93vh;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapComponent } from './map.component';
describe('MapComponent', () => {
let component: MapComponent;
let fixture: ComponentFixture<MapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import {Component, OnInit} from '@angular/core';
import {MapService} from '../service/map.service';
@Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements OnInit {
constructor(private service: MapService) {
}
ngOnInit(): void {
this.initMapView();
}
async initMapView(): Promise<any> {
this.service.initMap(51.509865, -0.118092, 14);
await this.service.drawStationMarkers();
this.service.drawHeatmap();
this.service.drawAccidents();
}
}

View File

@ -0,0 +1,25 @@
<mat-card class="mat-elevation-z0 p-0">
<mat-card-header>
<mat-card-title>{{station?.commonName}}</mat-card-title>
</mat-card-header>
<mat-card-content class="d-flex flex-column align-items-center justify-content-center">
<div id="chart" class="w-100 ml-n4">
<apx-chart
class="w-100"
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[title]="chartOptions.title"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"
></apx-chart>
</div>
<button (click)="route()" mat-raised-button id="route-button">Open in dashboard</button>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,14 @@
#route-button {
margin: 10px 0 0 0;
padding: 0;
}
.mat-card {
width: 30em;
}
#route-button {
padding: 0 10px;
background-color: #017bfe;
color: white;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PopUpComponent } from './pop-up.component';
describe('PopUpComponent', () => {
let component: PopUpComponent;
let fixture: ComponentFixture<PopUpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PopUpComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PopUpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,118 @@
import {Component, OnInit} from '@angular/core';
import {IMapBikePoint} from '../../service/domain/map-bike-point';
import {Router} from '@angular/router';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
export type ChartOptions = {
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
xaxis: ApexXAxis;
yaxis: ApexYAxis;
stroke: ApexStroke;
title: ApexTitleSubtitle;
tooltip: ApexTooltip;
fill: ApexFill;
legend: ApexLegend;
};
@Component({
selector: 'app-pop-up',
templateUrl: './pop-up.component.html',
styleUrls: ['./pop-up.component.scss']
})
export class PopUpComponent implements OnInit {
station: IMapBikePoint;
public chartOptions: Partial<ChartOptions>;
constructor(private router: Router) {
}
ngOnInit(): void {
const NbBlockedDocks = this.station.status.NbDocks - this.station.status.NbBikes - this.station.status.NbEmptyDocks;
this.chartOptions = {
series: [
{
name: 'Bikes',
data: [this.station.status.NbBikes]
},
{
name: 'Empty docks',
data: [this.station.status.NbEmptyDocks]
},
{
name: 'Blocked docks',
data: [NbBlockedDocks]
}
],
colors: ['#51ca49', '#8f8e8e', '#f00'],
chart: {
type: 'bar',
height: 125,
stacked: true,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: true
}
},
stroke: {
show: false
},
xaxis: {
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
title: {
text: undefined
},
axisBorder: {
show: false
},
min: 0,
max: this.station.status.NbDocks
},
tooltip: {
enabled: false,
},
fill: {
opacity: 1
},
legend: {
position: 'bottom',
horizontalAlign: 'right'
}
};
}
public route(): void {
this.router.navigate(['/dashboard', this.station.id]);
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DashboardService } from './dashboard.service';
describe('DashboardService', () => {
let service: DashboardService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DashboardService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class DashboardService {
constructor(private client: HttpClient) {
}
public fetchDashboardInit(id: string): Promise<any> {
return this.client.get(environment.apiUrl + `latest/dashboard/${id}/`).toPromise();
}
public fetchBikePointForStatus(id: string): Promise<any> {
return this.client.get(environment.apiUrl + `latest/bikepoints/${id}/`).toPromise();
}
public fetchDashboardStationTo(id: string, startDate: string, endDate: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/to?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
public fetchDashboardStationFrom(id: string, startDate: string, endDate: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/from?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
public fetchDashboardStationCharts(id: string, startDate: string, endDate: string, type: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/${type}?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
}

View File

@ -0,0 +1,22 @@
export interface IDashboardCommonBikePoint {
id?: string;
color?: string;
commonName?: string;
lat?: number;
lon?: number;
maxEndDate?: string;
maxStartDate?: string;
}
export class DashboardCommonBikePoint implements IDashboardCommonBikePoint {
constructor(
public id?: string,
public color?: string,
public commonName?: string,
public lat?: number,
public lon?: number,
public maxEndDate?: string,
public maxStartDate?: string
) {
}
}

View File

@ -0,0 +1,24 @@
export interface IMapBikePoint {
id?: string;
commonName?: string;
lat?: number;
lon?: number;
status?: BikePointStatus;
}
export class MapBikePoint implements IMapBikePoint {
constructor(
public id?: string,
public commonName?: string,
public lat?: number,
public lon?: number,
public status?: BikePointStatus
) {
}
}
export class BikePointStatus {
NbBikes?: number;
NbEmptyDocks?: number;
NbDocks?: number;
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { MapService } from './map.service';
describe('MapService', () => {
let service: MapService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MapService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,257 @@
import {Injectable} from '@angular/core';
import * as L from 'leaflet';
import 'leaflet.markercluster';
import 'leaflet.heat/dist/leaflet-heat';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
import {PopUpService} from './pop-up.service';
import {IMapBikePoint} from './domain/map-bike-point';
import {IDashboardCommonBikePoint} from './domain/dashboard-common-bike-point';
const createIcon = color => L.icon({
iconUrl: `../../assets/bike-point-${color}.png`,
iconSize: [45, 45],
iconAnchor: [23, 36],
popupAnchor: [0, -29]
});
@Injectable({
providedIn: 'root'
})
export class MapService {
public map;
public miniMap;
bikePoints: Array<IMapBikePoint> = [];
mapOverlays: any = {};
layerToDisplay: any = {};
miniMapMarker: L.layerGroup;
markerLayer = [];
polylineLayer = [];
dashBoardMarker = L.marker;
dashBoardBikePoint: IDashboardCommonBikePoint;
layerControl = L.control(null);
legend = L.control({position: 'bottomleft'});
accidentLegend = L.control({position: 'bottomleft'});
constructor(
private client: HttpClient,
private popUpService: PopUpService
) {
}
public async autoRefresh(): Promise<any> {
this.layerToDisplay = {};
for (const name in this.mapOverlays) {
if (this.map.hasLayer(this.mapOverlays[name])) {
if (this.mapOverlays.Heatmap === this.mapOverlays[name]) {
this.layerToDisplay.Heatmap = this.mapOverlays[name];
} else if (this.mapOverlays.Accidents === this.mapOverlays[name]) {
this.layerToDisplay.Accidents = this.mapOverlays[name];
}
}
this.map.removeLayer(this.mapOverlays[name]);
}
await this.drawStationMarkers();
this.drawHeatmap();
this.drawAccidents();
}
public initMap(lat: number, lon: number, zoom: number): void {
this.map = L.map('map').setView([lat, lon], zoom);
this.map.addLayer(new L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
minZoom: 0,
maxZoom: 19,
preferCanvas: true
}));
this.accidentLegend.onAdd = () => {
const getCircle = (color) => {
return `
<svg height="16" width="16">
<circle cx="8" cy="8" r="8" stroke="black" stroke-width="1" fill=${color} />
</svg>`;
};
const div = L.DomUtil.create('div', 'legend legend-accidents');
div.innerHTML = `
<h4>Accident severities</h4>
<div>
${getCircle('yellow')}<span>Slight accident</span>
</div>
<div>
${getCircle('orange')}<span>Severe accident</span>
</div>
<div>
${getCircle('red')}<span>Fatal accident</span>
</div>
`;
return div;
};
this.map.on('overlayadd', e => e.name === 'Accidents' ? this.accidentLegend.addTo(this.map) : null);
this.map.on('overlayremove', e => e.name === 'Accidents' ? this.accidentLegend.remove() : null);
}
public initDashboardMap(lat: number, lon: number, zoom: number): void {
if (this.miniMap) {
this.miniMap.off();
this.miniMap.remove();
}
this.miniMap = L.map('minimap').setView([lat, lon], zoom);
this.miniMap.addLayer(new L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
minZoom: 0,
maxZoom: 19
}));
}
public drawStationMarkers(): Promise<any> {
return this.fetchBikePointGeoData().then((data) => {
this.bikePoints = data;
const markerClusters = L.markerClusterGroup({
spiderflyOnMaxZoom: true,
showCoverageOnHover: true,
zoomToBoundsOnClick: true
});
this.mapOverlays.Bikepoints = markerClusters;
this.map.addLayer(markerClusters);
for (const station of data) {
const marker = L.marker([station.lat, station.lon], {icon: createIcon('blue')});
markerClusters.addLayer(marker);
marker.on('click', e => {
e.target.bindPopup(this.popUpService.makeAvailabilityPopUp(station), {maxWidth: 'auto'})
.openPopup();
this.map.panTo(e.target.getLatLng());
});
marker.on('popupclose', e => e.target.unbindPopup());
}
}).catch((error) => {
console.log(error);
});
}
public drawHeatmap(): void {
const heatPoints = this.bikePoints.map(bikePoint => ([
bikePoint.lat,
bikePoint.lon,
bikePoint.status.NbBikes
]));
const heatmap = L.heatLayer(heatPoints, {
max: 5,
radius: 90
});
if (this.layerToDisplay.Heatmap) {
this.map.addLayer(heatmap);
}
this.mapOverlays.Heatmap = heatmap;
}
public drawAccidents(): void {
this.fetchAccidentGeoData().then(data => {
const myRenderer = L.canvas({padding: 0.5});
const accidents = [];
for (const accident of data) {
const severityColor = this.getAccidentColor(accident.severity);
const accidentMarker = L.circle([accident.lat, accident.lon], {
renderer: myRenderer,
color: severityColor,
fillColor: severityColor,
fillOpacity: 0.5,
radius: 30,
interactive: false
});
accidents.push(accidentMarker);
}
const accidentLayer = L.layerGroup(accidents);
if (this.layerToDisplay.Accidents) {
this.map.addLayer(accidentLayer);
}
this.mapOverlays.Accidents = accidentLayer;
this.drawMapControl();
});
}
public getAccidentColor(severity: string): string {
switch (severity) {
case 'Slight':
return 'yellow';
case 'Serious':
return 'orange';
case 'Fatal':
return 'red';
}
}
public drawDashboardStationMarker(station: IDashboardCommonBikePoint): void {
this.dashBoardBikePoint = station;
this.dashBoardMarker = L.marker([station.lat, station.lon], {icon: createIcon('blue')}).addTo(this.miniMap);
this.dashBoardMarker.on('mouseover', e => e.target.bindPopup(`<p>${station.commonName}</p>`).openPopup());
this.dashBoardMarker.on('mouseout', e => e.target.closePopup());
}
public drawTableStationMarker(bikePoints: any[]): void {
this.removeOverlayOnMiniMap();
for (const point of bikePoints) {
const marker = L.marker([point.stationLat, point.stationLon], {icon: createIcon(point.color)}).addTo(this.miniMap);
marker.on('mouseover', e => e.target.bindPopup(`<p>${point.stationName}</p>`).openPopup());
marker.on('mouseout', e => e.target.closePopup());
this.drawLineOnMiniMap(marker, point);
this.markerLayer.push(marker);
}
this.miniMap.fitBounds(L.featureGroup([this.dashBoardMarker, ...this.markerLayer]).getBounds());
if (this.polylineLayer.length === 0) {
this.legend.remove();
} else {
this.drawLegend();
}
}
drawLegend(): void {
this.legend.onAdd = () => {
const div = L.DomUtil.create('div', 'legend');
div.innerHTML += `<h4>Traffic lines</h4>`;
div.innerHTML += `<i style="background: green"></i><span>inbound</span><br>`;
div.innerHTML += `<i style="background: red"></i><span>outbound</span><br>`;
div.innerHTML += `<i style="background: blue"></i><span>in- and outbound</span>`;
return div;
};
this.legend.addTo(this.miniMap);
}
public removeOverlayOnMiniMap(): void {
if (this.markerLayer) {
this.markerLayer.forEach(marker => {
this.miniMap.removeLayer(marker);
});
this.markerLayer = [];
this.polylineLayer.forEach(polyline => {
this.miniMap.removeLayer(polyline);
});
this.polylineLayer = [];
}
this.legend.remove();
}
private drawLineOnMiniMap(marker: L.marker, bikePoint: any): void {
const latlngs = [];
latlngs.push(this.dashBoardMarker.getLatLng());
latlngs.push(marker.getLatLng());
this.polylineLayer.push(L.polyline(latlngs, {color: bikePoint.polyLineColor}).addTo(this.miniMap));
}
private drawMapControl(): void {
this.map.removeControl(this.layerControl);
this.layerControl = L.control.layers(null, this.mapOverlays, {position: 'bottomright'});
this.map.addControl(this.layerControl);
}
private fetchBikePointGeoData(): Promise<any> {
return this.client.get(environment.apiUrl + 'latest/bikepoints/').toPromise();
}
private fetchAccidentGeoData(): Promise<any> {
return this.client.get(environment.apiUrl + 'latest/accidents/2019').toPromise();
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { PopUpService } from './pop-up.service';
describe('PopUpService', () => {
let service: PopUpService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PopUpService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import {ComponentFactoryResolver, Injectable, Injector} from '@angular/core';
import {IMapBikePoint} from './domain/map-bike-point';
import {PopUpComponent} from '../map/pop-up/pop-up.component';
@Injectable({
providedIn: 'root'
})
export class PopUpService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {
}
makeAvailabilityPopUp(station: IMapBikePoint): any {
const factory = this.componentFactoryResolver.resolveComponentFactory(PopUpComponent);
const component = factory.create(this.injector);
component.instance.station = station;
component.changeDetectorRef.detectChanges();
return component.location.nativeElement;
}
}

View File

@ -0,0 +1,13 @@
<div class="d-flex">
<a color="primary"
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203"
mat-flat-button
target="_blank">
<mat-icon>library_books</mat-icon>
Wiki
</a>
<a color="primary" mat-flat-button routerLink="/">
<mat-icon>map</mat-icon>
back to map
</a>
</div>

View File

@ -0,0 +1,4 @@
a:hover {
background: #086ed2;
text-decoration: none;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardInteractionComponent } from './dashboard-interaction.component';
describe('DashboardInteractionComponent', () => {
let component: DashboardInteractionComponent;
let fixture: ComponentFixture<DashboardInteractionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardInteractionComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardInteractionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dashboard-interaction',
templateUrl: './dashboard-interaction.component.html',
styleUrls: ['./dashboard-interaction.component.scss']
})
export class DashboardInteractionComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,9 @@
<div class="d-flex">
<app-auto-refresh></app-auto-refresh>
<a class="button-wiki" color="primary"
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203" mat-flat-button
target="_blank">
<mat-icon>library_books</mat-icon>
Wiki
</a>
</div>

View File

@ -0,0 +1,4 @@
.button-wiki:hover {
background: #086ed2;
text-decoration: none;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapInteractionComponent } from './map-interaction.component';
describe('InteractionComponent', () => {
let component: MapInteractionComponent;
let fixture: ComponentFixture<MapInteractionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapInteractionComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapInteractionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-map-interaction',
templateUrl: './map-interaction.component.html',
styleUrls: ['./map-interaction.component.scss']
})
export class MapInteractionComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,13 @@
<mat-toolbar class="mat-toolbar" color="primary">
<span routerLink="/" id="logo">
<img alt="" src="../../assets/Logo.png" height="40" class="mx-2 mb-1">
Bike Stations in London
</span>
<span class="toolbar-spacer"></span>
<div *ngIf="!hasRoute('dashboard')">
<app-map-interaction></app-map-interaction>
</div>
<div *ngIf="hasRoute('dashboard')">
<app-dashboard-interaction></app-dashboard-interaction>
</div>
</mat-toolbar>

View File

@ -0,0 +1,7 @@
.toolbar-spacer {
flex: 1 1 auto;
}
.mat-toolbar {
height: 5vh;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarComponent } from './toolbar.component';
describe('ToolbarComponent', () => {
let component: ToolbarComponent;
let fixture: ComponentFixture<ToolbarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ToolbarComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ToolbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import {Router} from '@angular/router';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit(): void {
}
hasRoute(route: string): boolean {
return this.router.url.includes(route);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
apiUrl: 'https://it-schwarz.net/api/'
};

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl: 'https://it-schwarz.net/api/'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

Some files were not shown because too many files have changed in this diff Show More