Compare commits
117 Commits
postgres-d
...
master
Author | SHA1 | Date | |
---|---|---|---|
190985d42c | |||
7695a6838f | |||
|
2f9b7f48bc | ||
4b91b4401f | |||
b0741fc0fe | |||
b9c628b505 | |||
5bc58b8df9 | |||
77444dec89 | |||
cf6ab406f4 | |||
94c3fb99f4 | |||
1545ce1a78 | |||
|
47d53ecc54 | ||
|
2edc3b0a83 | ||
|
83e136b61c | ||
|
a8bff8965c | ||
860d351323 | |||
|
b6b02f2c01 | ||
9686dc2ea5 | |||
493cdd9ea7 | |||
|
abfc5424a3 | ||
|
8ef1b34c3b | ||
|
210061442f | ||
89d4738da5 | |||
8724eaf914 | |||
c73d99bc51 | |||
|
52e68a4f28 | ||
|
c91a0f9872 | ||
|
f2dd47684b | ||
|
9c38fe4c76 | ||
|
a3bf06075e | ||
|
dcd5cb72c5 | ||
|
bd21861368 | ||
|
16468ba950 | ||
|
e6f5407319 | ||
|
cdea238830 | ||
|
6d7c40ada6 | ||
|
cdbf36fadb | ||
|
9c53c382ff | ||
|
7a80335860 | ||
|
82b8b8bd74 | ||
|
01e47b9656 | ||
|
5e4952b08e | ||
|
f5924404a7 | ||
|
c05ee5388e | ||
|
4525c10207 | ||
|
b9080f64b0 | ||
|
a3045f406c | ||
|
8de1c8cfc3 | ||
84c9016be4 | |||
|
5e71b3c094 | ||
|
953b75d55e | ||
|
bba88ac1aa | ||
|
a597343b0c | ||
|
1b19013b0a | ||
|
3f3dd14e8f | ||
|
1b82cd6b8f | ||
|
3600f3a6e3 | ||
|
8a69ffe29e | ||
|
42735b4c90 | ||
|
3b0889862c | ||
|
46c5d2192a | ||
8947f9116e | |||
|
9afb135d91 | ||
|
b484857746 | ||
|
600015d1bd | ||
|
cff34ac31d | ||
|
4503274a2e | ||
|
5358bb21db | ||
|
110f8f2595 | ||
|
f1f82b9dd1 | ||
13f06a0ce5 | |||
44da56f823 | |||
|
570b397173 | ||
|
59399fcc75 | ||
|
7372a1bc87 | ||
|
e8e6b3bb9c | ||
|
7ca560ec2f | ||
|
531c83db97 | ||
|
945642db5a | ||
|
f64fe65ed6 | ||
|
02ff4267bf | ||
3cdfac30bc | |||
|
b19da0c819 | ||
|
d3f40f994c | ||
|
575dad8646 | ||
7a85fe933c | |||
|
7904963b3e | ||
|
b6dd2ee825 | ||
|
678272ef8a | ||
|
8dd31f3703 | ||
|
bcdd859be9 | ||
|
b51e533834 | ||
|
b3d6ee8473 | ||
|
6581b621fe | ||
|
281fb3ae40 | ||
|
7c1612f8da | ||
|
1e755566e3 | ||
|
af540c5f09 | ||
|
6698381f85 | ||
|
3eb3570370 | ||
|
ba0f7b5e86 | ||
|
95baf1f9b7 | ||
|
22cd28e2b3 | ||
|
18ceed31b1 | ||
|
4fb25b3750 | ||
|
bb5921fe15 | ||
|
82c5c3e3c0 | ||
|
4e46674c07 | ||
|
f69c999f02 | ||
|
711f3457b6 | ||
|
ab4ffaab87 | ||
|
a72a454f3d | ||
|
6b0a804aa0 | ||
|
b140b1b6bd | ||
|
064bd86f2b | ||
9cdae30f7e | |||
04c45e0b7a |
@ -8,5 +8,6 @@ Teammitglieder:
|
||||
http://marcel.schwarz.gitlab.io/geovisualisierung/project-1/
|
||||
## Projekt 2
|
||||
http://marcel.schwarz.gitlab.io/geovisualisierung/project-2/
|
||||
|
||||
## Projekt 3
|
||||
https://it-schwarz.net
|
||||
|
||||
|
@ -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
|
||||
```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.
|
||||
|
||||
To stop just shut down the container.
|
||||
To stop just shut down the container.
|
||||
|
@ -13,9 +13,7 @@ app = FastAPI(
|
||||
|
||||
origins = [
|
||||
"http://it-schwarz.net",
|
||||
"https://it-schwarz.net",
|
||||
"http://localhost",
|
||||
"http://localhost:4200",
|
||||
"https://it-schwarz.net"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
|
@ -37,36 +37,50 @@ def get_dashboard(station_id):
|
||||
JOIN bike_points b ON u.start_station_id = b.id_num
|
||||
JOIN dashboard d ON u.start_station_id = d.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):
|
||||
query = """
|
||||
SELECT
|
||||
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 = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||
GROUP BY u.end_station_name
|
||||
ORDER BY number DESC
|
||||
LIMIT 3"""
|
||||
topPoints.*,
|
||||
b.lat AS stationLat,
|
||||
b.lon AS stationLon
|
||||
FROM (
|
||||
SELECT
|
||||
u.end_station_name AS stationName,
|
||||
u.end_station_id AS stationId,
|
||||
count(*) AS number,
|
||||
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()
|
||||
|
||||
|
||||
def get_dashboard_from(station_id, start_date, end_date):
|
||||
query = """
|
||||
SELECT
|
||||
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.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||
GROUP BY u.start_station_name
|
||||
ORDER BY number DESC
|
||||
LIMIT 3"""
|
||||
topPoints.*,
|
||||
b.lat AS stationLat,
|
||||
b.lon AS stationLon
|
||||
FROM (
|
||||
SELECT
|
||||
u.start_station_name AS stationName,
|
||||
u.start_station_id AS stationId,
|
||||
count(*) AS number,
|
||||
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()
|
||||
|
||||
|
||||
|
@ -2,8 +2,6 @@ import csv
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
@ -61,34 +59,34 @@ def get_online_files_list(subdir_filter=None, file_extension_filter=None):
|
||||
|
||||
def init_database():
|
||||
LOG.info("Try to create tables")
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
||||
rental_id BIGINT PRIMARY KEY,
|
||||
duration BIGINT,
|
||||
bike_id BIGINT,
|
||||
end_date TIMESTAMP,
|
||||
end_station_id BIGINT,
|
||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||
conn.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
||||
rental_id INTEGER PRIMARY KEY,
|
||||
duration INTEGER,
|
||||
bike_id INTEGER,
|
||||
end_date INTEGER,
|
||||
end_station_id INTEGER,
|
||||
end_station_name TEXT,
|
||||
start_date TIMESTAMP,
|
||||
start_station_id BIGINT,
|
||||
start_date INTEGER,
|
||||
start_station_id INTEGER,
|
||||
start_station_name TEXT
|
||||
)""")
|
||||
cursor.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 read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
|
||||
conn.execute("""CREATE TABLE IF NOT EXISTS bike_points(
|
||||
id TEXT PRIMARY KEY,
|
||||
common_name TEXT,
|
||||
lat REAL,
|
||||
lon REAL,
|
||||
id_num INTEGER
|
||||
)""")
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS accidents(
|
||||
conn.execute("""CREATE TABLE IF NOT EXISTS accidents(
|
||||
id INTEGER PRIMARY KEY,
|
||||
lat REAL,
|
||||
lon REAL,
|
||||
location TEXT,
|
||||
date TIMESTAMP,
|
||||
severity TEXT
|
||||
date TEXT,
|
||||
severity TEXT,
|
||||
UNIQUE (lat, lon, date)
|
||||
)""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@ -134,23 +132,20 @@ def create_dashboard_table():
|
||||
|
||||
def import_bikepoints():
|
||||
LOG.info("Importing bikepoints")
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||
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))
|
||||
|
||||
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.close()
|
||||
LOG.info("Bikepoints imported")
|
||||
|
||||
|
||||
def import_accidents(year):
|
||||
LOG.info("Importing accidents")
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor()
|
||||
LOG.info(f"Importing accidents for year {year}")
|
||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||
|
||||
def filter_pedal_cycles(accident):
|
||||
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 = json.loads(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")
|
||||
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.close()
|
||||
LOG.info("Accidents importet")
|
||||
LOG.info(f"Accidents imported for year {year}")
|
||||
|
||||
|
||||
def import_usage_stats_file(export_file: ApiExportFile):
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor()
|
||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||
|
||||
cursor.execute("SELECT * FROM read_files WHERE etag = %s", (export_file.etag,))
|
||||
if len(cursor.fetchall()) != 0:
|
||||
rows = conn.execute("SELECT * FROM read_files WHERE etag LIKE ?", (export_file.etag,)).fetchall()
|
||||
if len(rows) != 0:
|
||||
LOG.warning(f"Skipping import of {export_file.path}")
|
||||
return
|
||||
|
||||
@ -196,13 +191,13 @@ def import_usage_stats_file(export_file: ApiExportFile):
|
||||
# Bike Id
|
||||
int(entry[2] or "-1"),
|
||||
# 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
|
||||
int(entry[4] or "-1"),
|
||||
# EndStation Name
|
||||
entry[5].strip(),
|
||||
# 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
|
||||
int(entry[7]),
|
||||
# StartStation Name
|
||||
@ -215,45 +210,36 @@ def import_usage_stats_file(export_file: ApiExportFile):
|
||||
LOG.error(f"Key Error {e} on line {entry}")
|
||||
return
|
||||
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)
|
||||
cursor.execute("INSERT INTO read_files VALUES (%s, %s) ON CONFLICT DO NOTHING", (export_file.path, export_file.etag))
|
||||
conn.executemany("INSERT OR IGNORE INTO usage_stats VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", mapped)
|
||||
conn.execute("INSERT OR IGNORE INTO read_files VALUES (?, ?)", (export_file.path, export_file.etag))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
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():
|
||||
# General DB init
|
||||
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
|
||||
all_files = get_online_files_list(subdir_filter="usage-stats", file_extension_filter=".csv")
|
||||
for file in all_files:
|
||||
import_usage_stats_file(file)
|
||||
#
|
||||
# count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
||||
#
|
||||
# # Create search-index for faster querying
|
||||
# create_indexes()
|
||||
# # Import Bikepoints
|
||||
# import_bikepoints()
|
||||
# # Import bike accidents
|
||||
# import_accidents(2019)
|
||||
#
|
||||
# if count_after - count_pre > 0:
|
||||
# create_dashboard_table()
|
||||
|
||||
count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
||||
|
||||
# Create search-index for faster querying
|
||||
create_indexes()
|
||||
# Import Bikepoints
|
||||
import_bikepoints()
|
||||
# Import bike accidents
|
||||
for year in range(2005, 2020):
|
||||
import_accidents(year)
|
||||
|
||||
if count_after - count_pre > 0:
|
||||
create_dashboard_table()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
@ -10,10 +11,16 @@ router = APIRouter(prefix="/accidents", tags=["accidents", "local"])
|
||||
LOG = logging.getLogger()
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
slight = "Slight"
|
||||
serious = "Serious"
|
||||
fatal = "Fatal"
|
||||
|
||||
|
||||
class Accident(BaseModel):
|
||||
lat: float
|
||||
lon: float
|
||||
severity: str
|
||||
severity: Severity
|
||||
|
||||
|
||||
@router.get(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import datetime
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
@ -9,7 +9,7 @@ import api_database
|
||||
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
|
||||
if days_requested < 0:
|
||||
raise HTTPException(status_code=400, detail="Requested date-range is negative")
|
||||
@ -20,30 +20,32 @@ class StationDashboard(BaseModel):
|
||||
commonName: Optional[str]
|
||||
lat: Optional[float]
|
||||
lon: Optional[float]
|
||||
maxEndDate: Optional[datetime.date]
|
||||
maxStartDate: Optional[datetime.date]
|
||||
maxEndDate: Optional[date]
|
||||
maxStartDate: Optional[date]
|
||||
|
||||
|
||||
@router.get("/", response_model=StationDashboard)
|
||||
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):
|
||||
startStationName: str
|
||||
endStationName: str
|
||||
stationName: str
|
||||
stationId: int
|
||||
stationLat: float
|
||||
stationLon: float
|
||||
number: int
|
||||
avgDuration: int
|
||||
|
||||
|
||||
@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)
|
||||
return api_database.get_dashboard_to(station_id, start_date, end_date)
|
||||
|
||||
|
||||
@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)
|
||||
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])
|
||||
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)
|
||||
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):
|
||||
@ -66,6 +80,18 @@ class StationDashboardTimeGroup(BaseModel):
|
||||
|
||||
|
||||
@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)
|
||||
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
|
||||
|
@ -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
|
18
projects/project-3/frontend/.browserslistrc
Normal 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.
|
16
projects/project-3/frontend/.editorconfig
Normal 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
@ -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
|
27
projects/project-3/frontend/README.md
Normal 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.
|
144
projects/project-3/frontend/angular.json
Normal 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
|
||||
}
|
||||
}
|
36
projects/project-3/frontend/e2e/protractor.conf.js
Normal 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
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
23
projects/project-3/frontend/e2e/src/app.e2e-spec.ts
Normal 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));
|
||||
});
|
||||
});
|
11
projects/project-3/frontend/e2e/src/app.po.ts
Normal 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>;
|
||||
}
|
||||
}
|
14
projects/project-3/frontend/e2e/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
32
projects/project-3/frontend/karma.conf.js
Normal 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
57
projects/project-3/frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
17
projects/project-3/frontend/src/app/app-routing.module.ts
Normal 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 {
|
||||
}
|
3
projects/project-3/frontend/src/app/app.component.html
Normal file
@ -0,0 +1,3 @@
|
||||
<app-toolbar></app-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
35
projects/project-3/frontend/src/app/app.component.spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
14
projects/project-3/frontend/src/app/app.component.ts
Normal 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');
|
||||
}
|
||||
}
|
90
projects/project-3/frontend/src/app/app.module.ts
Normal 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 {
|
||||
}
|
@ -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>
|
@ -0,0 +1 @@
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<mat-card class="p-0">
|
||||
<div id="minimap" style="height: 30rem"></div>
|
||||
</mat-card>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
.header-image {
|
||||
background-image: url('../../../assets/bike-point-blue.png');
|
||||
background-size: cover;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<div class="footer d-flex justify-content-center align-items-center">
|
||||
<div class="copyright">
|
||||
<span>© Tim Herbst & Marcel Schwarz</span>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
.footer {
|
||||
height: 2vh;
|
||||
background: #2f2f2f;
|
||||
|
||||
.copyright {
|
||||
color: white;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<mat-slide-toggle [(ngModel)]="isFlagActive" (ngModelChange)="onChange($event)" color="warn">
|
||||
auto refresh
|
||||
</mat-slide-toggle>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
mat-slide-toggle {
|
||||
margin-right: 1em;
|
||||
font-size: 15px;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<div class="map-container" fxLayout="row">
|
||||
<div class="map-frame" fxFill>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
#map {
|
||||
height: 93vh;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
26
projects/project-3/frontend/src/app/map/map.component.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
) {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
257
projects/project-3/frontend/src/app/service/map.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
a:hover {
|
||||
background: #086ed2;
|
||||
text-decoration: none;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -0,0 +1,4 @@
|
||||
.button-wiki:hover {
|
||||
background: #086ed2;
|
||||
text-decoration: none;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -0,0 +1,7 @@
|
||||
.toolbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.mat-toolbar {
|
||||
height: 5vh;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
0
projects/project-3/frontend/src/assets/.gitkeep
Normal file
BIN
projects/project-3/frontend/src/assets/Logo.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-black.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-blue.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-gray.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-green.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-orange.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-purple.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-red.png
Normal file
After Width: | Height: | Size: 76 KiB |
@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl: 'https://it-schwarz.net/api/'
|
||||
};
|
17
projects/project-3/frontend/src/environments/environment.ts
Normal 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.
|
BIN
projects/project-3/frontend/src/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
15
projects/project-3/frontend/src/index.html
Normal 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>
|
12
projects/project-3/frontend/src/main.ts
Normal 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));
|