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/
|
http://marcel.schwarz.gitlab.io/geovisualisierung/project-1/
|
||||||
## Projekt 2
|
## Projekt 2
|
||||||
http://marcel.schwarz.gitlab.io/geovisualisierung/project-2/
|
http://marcel.schwarz.gitlab.io/geovisualisierung/project-2/
|
||||||
|
## Projekt 3
|
||||||
|
https://it-schwarz.net
|
||||||
|
|
||||||
|
@ -25,9 +25,9 @@ docker build -t geovis-backend .
|
|||||||
```
|
```
|
||||||
After the build make sure you are in the same directory as "bike-data.db" resides, if so, run
|
After the build make sure you are in the same directory as "bike-data.db" resides, if so, run
|
||||||
```shell
|
```shell
|
||||||
docker run -v $(pwd):/app -p 8080:80 --restart always -d test
|
docker run -v $(pwd):/app -p 8080:80 --restart always -d geovis-backend
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `$(pwd)` puts the current directory in the command, if you are on Windows, you can use WSL or provide the full path by typing it out.
|
Note: `$(pwd)` puts the current directory in the command, if you are on Windows, you can use WSL or provide the full path by typing it out.
|
||||||
|
|
||||||
To stop just shut down the container.
|
To stop just shut down the container.
|
||||||
|
@ -13,9 +13,7 @@ app = FastAPI(
|
|||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
"http://it-schwarz.net",
|
"http://it-schwarz.net",
|
||||||
"https://it-schwarz.net",
|
"https://it-schwarz.net"
|
||||||
"http://localhost",
|
|
||||||
"http://localhost:4200",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
@ -37,36 +37,50 @@ def get_dashboard(station_id):
|
|||||||
JOIN bike_points b ON u.start_station_id = b.id_num
|
JOIN bike_points b ON u.start_station_id = b.id_num
|
||||||
JOIN dashboard d ON u.start_station_id = d.id
|
JOIN dashboard d ON u.start_station_id = d.id
|
||||||
WHERE u.start_station_id = ?"""
|
WHERE u.start_station_id = ?"""
|
||||||
return get_db_connection().execute(query, (station_id,)).fetchall()
|
return get_db_connection().execute(query, (station_id,)).fetchone()
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard_to(station_id, start_date, end_date):
|
def get_dashboard_to(station_id, start_date, end_date):
|
||||||
query = """
|
query = """
|
||||||
SELECT
|
SELECT
|
||||||
u.start_station_name AS startStationName,
|
topPoints.*,
|
||||||
u.end_station_name AS endStationName,
|
b.lat AS stationLat,
|
||||||
count(*) AS number,
|
b.lon AS stationLon
|
||||||
round(avg(u.duration)) AS avgDuration
|
FROM (
|
||||||
FROM usage_stats u
|
SELECT
|
||||||
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
u.end_station_name AS stationName,
|
||||||
GROUP BY u.end_station_name
|
u.end_station_id AS stationId,
|
||||||
ORDER BY number DESC
|
count(*) AS number,
|
||||||
LIMIT 3"""
|
round(avg(u.duration)) AS avgDuration
|
||||||
|
FROM usage_stats u
|
||||||
|
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||||
|
GROUP BY u.end_station_name
|
||||||
|
ORDER BY number DESC
|
||||||
|
LIMIT 3
|
||||||
|
) as topPoints
|
||||||
|
JOIN bike_points b ON b.id_num = topPoints.stationId"""
|
||||||
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
|
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard_from(station_id, start_date, end_date):
|
def get_dashboard_from(station_id, start_date, end_date):
|
||||||
query = """
|
query = """
|
||||||
SELECT
|
SELECT
|
||||||
u.start_station_name AS startStationName,
|
topPoints.*,
|
||||||
u.end_station_name AS endStationName,
|
b.lat AS stationLat,
|
||||||
count(*) AS number,
|
b.lon AS stationLon
|
||||||
round(avg(u.duration)) AS avgDuration
|
FROM (
|
||||||
FROM usage_stats u
|
SELECT
|
||||||
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
u.start_station_name AS stationName,
|
||||||
GROUP BY u.start_station_name
|
u.start_station_id AS stationId,
|
||||||
ORDER BY number DESC
|
count(*) AS number,
|
||||||
LIMIT 3"""
|
round(avg(u.duration)) AS avgDuration
|
||||||
|
FROM usage_stats u
|
||||||
|
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||||
|
GROUP BY u.start_station_name
|
||||||
|
ORDER BY number DESC
|
||||||
|
LIMIT 3
|
||||||
|
) as topPoints
|
||||||
|
JOIN bike_points b ON b.id_num = topPoints.stationId"""
|
||||||
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
|
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,8 +2,6 @@ import csv
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import psycopg2
|
|
||||||
import psycopg2.extras
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@ -61,34 +59,34 @@ def get_online_files_list(subdir_filter=None, file_extension_filter=None):
|
|||||||
|
|
||||||
def init_database():
|
def init_database():
|
||||||
LOG.info("Try to create tables")
|
LOG.info("Try to create tables")
|
||||||
conn = get_conn()
|
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||||
cursor = conn.cursor()
|
conn.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
||||||
cursor.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
rental_id INTEGER PRIMARY KEY,
|
||||||
rental_id BIGINT PRIMARY KEY,
|
duration INTEGER,
|
||||||
duration BIGINT,
|
bike_id INTEGER,
|
||||||
bike_id BIGINT,
|
end_date INTEGER,
|
||||||
end_date TIMESTAMP,
|
end_station_id INTEGER,
|
||||||
end_station_id BIGINT,
|
|
||||||
end_station_name TEXT,
|
end_station_name TEXT,
|
||||||
start_date TIMESTAMP,
|
start_date INTEGER,
|
||||||
start_station_id BIGINT,
|
start_station_id INTEGER,
|
||||||
start_station_name TEXT
|
start_station_name TEXT
|
||||||
)""")
|
)""")
|
||||||
cursor.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
|
conn.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
|
||||||
cursor.execute("""CREATE TABLE IF NOT EXISTS bike_points(
|
conn.execute("""CREATE TABLE IF NOT EXISTS bike_points(
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
common_name TEXT,
|
common_name TEXT,
|
||||||
lat REAL,
|
lat REAL,
|
||||||
lon REAL,
|
lon REAL,
|
||||||
id_num INTEGER
|
id_num INTEGER
|
||||||
)""")
|
)""")
|
||||||
cursor.execute("""CREATE TABLE IF NOT EXISTS accidents(
|
conn.execute("""CREATE TABLE IF NOT EXISTS accidents(
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
lat REAL,
|
lat REAL,
|
||||||
lon REAL,
|
lon REAL,
|
||||||
location TEXT,
|
location TEXT,
|
||||||
date TIMESTAMP,
|
date TEXT,
|
||||||
severity TEXT
|
severity TEXT,
|
||||||
|
UNIQUE (lat, lon, date)
|
||||||
)""")
|
)""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@ -134,23 +132,20 @@ def create_dashboard_table():
|
|||||||
|
|
||||||
def import_bikepoints():
|
def import_bikepoints():
|
||||||
LOG.info("Importing bikepoints")
|
LOG.info("Importing bikepoints")
|
||||||
conn = get_conn()
|
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
points = json.loads(requests.get("https://api.tfl.gov.uk/BikePoint").text)
|
points = json.loads(requests.get("https://api.tfl.gov.uk/BikePoint").text)
|
||||||
points = list(map(lambda p: (p['id'], p['commonName'], p['lat'], p['lon'], int(p['id'][11:])), points))
|
points = list(map(lambda p: (p['id'], p['commonName'], p['lat'], p['lon'], int(p['id'][11:])), points))
|
||||||
|
|
||||||
LOG.info(f"Writing {len(points)} bikepoints to DB")
|
LOG.info(f"Writing {len(points)} bikepoints to DB")
|
||||||
cursor.executemany("INSERT INTO bike_points VALUES (%s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", points)
|
conn.executemany("INSERT OR IGNORE INTO bike_points VALUES (?, ?, ?, ?, ?)", points)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
LOG.info("Bikepoints imported")
|
LOG.info("Bikepoints imported")
|
||||||
|
|
||||||
|
|
||||||
def import_accidents(year):
|
def import_accidents(year):
|
||||||
LOG.info("Importing accidents")
|
LOG.info(f"Importing accidents for year {year}")
|
||||||
conn = get_conn()
|
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
def filter_pedal_cycles(accident):
|
def filter_pedal_cycles(accident):
|
||||||
for vehicle in accident['vehicles']:
|
for vehicle in accident['vehicles']:
|
||||||
@ -161,22 +156,22 @@ def import_accidents(year):
|
|||||||
accidents = requests.get(f"https://api.tfl.gov.uk/AccidentStats/{year}").text
|
accidents = requests.get(f"https://api.tfl.gov.uk/AccidentStats/{year}").text
|
||||||
accidents = json.loads(accidents)
|
accidents = json.loads(accidents)
|
||||||
accidents = list(filter(filter_pedal_cycles, accidents))
|
accidents = list(filter(filter_pedal_cycles, accidents))
|
||||||
accidents = list(map(lambda a: (a['id'], a['lat'], a['lon'], a['location'], a['date'], a['severity']), accidents))
|
accidents = list(map(lambda a: (a['lat'], a['lon'], a['location'], a['date'], a['severity']), accidents))
|
||||||
|
|
||||||
LOG.info(f"Writing {len(accidents)} bike accidents to DB")
|
LOG.info(f"Writing {len(accidents)} bike accidents to DB")
|
||||||
cursor.executemany("INSERT INTO accidents VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", accidents)
|
conn.executemany("""INSERT OR IGNORE INTO
|
||||||
|
accidents(lat, lon, location, date, severity)
|
||||||
|
VALUES (?, ?, ?, ?, ?)""", accidents)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
LOG.info("Accidents importet")
|
LOG.info(f"Accidents imported for year {year}")
|
||||||
|
|
||||||
|
|
||||||
def import_usage_stats_file(export_file: ApiExportFile):
|
def import_usage_stats_file(export_file: ApiExportFile):
|
||||||
conn = get_conn()
|
conn = sqlite3.connect(DB_NAME, timeout=300)
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute("SELECT * FROM read_files WHERE etag = %s", (export_file.etag,))
|
rows = conn.execute("SELECT * FROM read_files WHERE etag LIKE ?", (export_file.etag,)).fetchall()
|
||||||
if len(cursor.fetchall()) != 0:
|
if len(rows) != 0:
|
||||||
LOG.warning(f"Skipping import of {export_file.path}")
|
LOG.warning(f"Skipping import of {export_file.path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -196,13 +191,13 @@ def import_usage_stats_file(export_file: ApiExportFile):
|
|||||||
# Bike Id
|
# Bike Id
|
||||||
int(entry[2] or "-1"),
|
int(entry[2] or "-1"),
|
||||||
# End Date
|
# End Date
|
||||||
datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M") if entry[3] else None,
|
int(datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[3] else -1,
|
||||||
# EndStation Id
|
# EndStation Id
|
||||||
int(entry[4] or "-1"),
|
int(entry[4] or "-1"),
|
||||||
# EndStation Name
|
# EndStation Name
|
||||||
entry[5].strip(),
|
entry[5].strip(),
|
||||||
# Start Date
|
# Start Date
|
||||||
datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M") if entry[6] else None,
|
int(datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[6] else -1,
|
||||||
# StartStation Id
|
# StartStation Id
|
||||||
int(entry[7]),
|
int(entry[7]),
|
||||||
# StartStation Name
|
# StartStation Name
|
||||||
@ -215,45 +210,36 @@ def import_usage_stats_file(export_file: ApiExportFile):
|
|||||||
LOG.error(f"Key Error {e} on line {entry}")
|
LOG.error(f"Key Error {e} on line {entry}")
|
||||||
return
|
return
|
||||||
LOG.info(f"Writing {len(mapped)} entries to DB")
|
LOG.info(f"Writing {len(mapped)} entries to DB")
|
||||||
psycopg2.extras.execute_values(cursor, "INSERT INTO usage_stats VALUES %s ON CONFLICT DO NOTHING ", mapped, page_size=1_000_000)
|
conn.executemany("INSERT OR IGNORE INTO usage_stats VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", mapped)
|
||||||
cursor.execute("INSERT INTO read_files VALUES (%s, %s) ON CONFLICT DO NOTHING", (export_file.path, export_file.etag))
|
conn.execute("INSERT OR IGNORE INTO read_files VALUES (?, ?)", (export_file.path, export_file.etag))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
LOG.info(f"Finished import of {export_file.path}")
|
LOG.info(f"Finished import of {export_file.path}")
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
return psycopg2.connect(
|
|
||||||
host="localhost",
|
|
||||||
database="postgres",
|
|
||||||
user="postgres",
|
|
||||||
password="supersecure"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# General DB init
|
# General DB init
|
||||||
init_database()
|
init_database()
|
||||||
import_accidents(2019)
|
|
||||||
import_bikepoints()
|
|
||||||
|
|
||||||
# count_pre = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
count_pre = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
||||||
#
|
|
||||||
# Download and import opendata from S3 bucket
|
# Download and import opendata from S3 bucket
|
||||||
all_files = get_online_files_list(subdir_filter="usage-stats", file_extension_filter=".csv")
|
all_files = get_online_files_list(subdir_filter="usage-stats", file_extension_filter=".csv")
|
||||||
for file in all_files:
|
for file in all_files:
|
||||||
import_usage_stats_file(file)
|
import_usage_stats_file(file)
|
||||||
#
|
|
||||||
# count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
count_after = sqlite3.connect(DB_NAME, timeout=300).execute("SELECT count(*) FROM usage_stats").fetchone()[0]
|
||||||
#
|
|
||||||
# # Create search-index for faster querying
|
# Create search-index for faster querying
|
||||||
# create_indexes()
|
create_indexes()
|
||||||
# # Import Bikepoints
|
# Import Bikepoints
|
||||||
# import_bikepoints()
|
import_bikepoints()
|
||||||
# # Import bike accidents
|
# Import bike accidents
|
||||||
# import_accidents(2019)
|
for year in range(2005, 2020):
|
||||||
#
|
import_accidents(year)
|
||||||
# if count_after - count_pre > 0:
|
|
||||||
# create_dashboard_table()
|
if count_after - count_pre > 0:
|
||||||
|
create_dashboard_table()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from enum import Enum
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
@ -10,10 +11,16 @@ router = APIRouter(prefix="/accidents", tags=["accidents", "local"])
|
|||||||
LOG = logging.getLogger()
|
LOG = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(str, Enum):
|
||||||
|
slight = "Slight"
|
||||||
|
serious = "Serious"
|
||||||
|
fatal = "Fatal"
|
||||||
|
|
||||||
|
|
||||||
class Accident(BaseModel):
|
class Accident(BaseModel):
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
severity: str
|
severity: Severity
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import datetime
|
from datetime import date, datetime, time, timedelta
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
@ -9,7 +9,7 @@ import api_database
|
|||||||
router = APIRouter(prefix="/dashboard/{station_id}", tags=["dashboard", "local"])
|
router = APIRouter(prefix="/dashboard/{station_id}", tags=["dashboard", "local"])
|
||||||
|
|
||||||
|
|
||||||
def validate_daterange(start_date: datetime.date, end_date: datetime.date):
|
def validate_daterange(start_date: date, end_date: date):
|
||||||
days_requested = (end_date - start_date).days
|
days_requested = (end_date - start_date).days
|
||||||
if days_requested < 0:
|
if days_requested < 0:
|
||||||
raise HTTPException(status_code=400, detail="Requested date-range is negative")
|
raise HTTPException(status_code=400, detail="Requested date-range is negative")
|
||||||
@ -20,30 +20,32 @@ class StationDashboard(BaseModel):
|
|||||||
commonName: Optional[str]
|
commonName: Optional[str]
|
||||||
lat: Optional[float]
|
lat: Optional[float]
|
||||||
lon: Optional[float]
|
lon: Optional[float]
|
||||||
maxEndDate: Optional[datetime.date]
|
maxEndDate: Optional[date]
|
||||||
maxStartDate: Optional[datetime.date]
|
maxStartDate: Optional[date]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=StationDashboard)
|
@router.get("/", response_model=StationDashboard)
|
||||||
def get_general_dashboard(station_id: int):
|
def get_general_dashboard(station_id: int):
|
||||||
return api_database.get_dashboard(station_id)[0]
|
return api_database.get_dashboard(station_id) or {}
|
||||||
|
|
||||||
|
|
||||||
class StationDashboardTopStationsEntry(BaseModel):
|
class StationDashboardTopStationsEntry(BaseModel):
|
||||||
startStationName: str
|
stationName: str
|
||||||
endStationName: str
|
stationId: int
|
||||||
|
stationLat: float
|
||||||
|
stationLon: float
|
||||||
number: int
|
number: int
|
||||||
avgDuration: int
|
avgDuration: int
|
||||||
|
|
||||||
|
|
||||||
@router.get("/to", response_model=List[StationDashboardTopStationsEntry])
|
@router.get("/to", response_model=List[StationDashboardTopStationsEntry])
|
||||||
def get_to_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
def get_to_dashboard_for_station(station_id: int, start_date: date, end_date: date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
return api_database.get_dashboard_to(station_id, start_date, end_date)
|
return api_database.get_dashboard_to(station_id, start_date, end_date)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/from", response_model=List[StationDashboardTopStationsEntry])
|
@router.get("/from", response_model=List[StationDashboardTopStationsEntry])
|
||||||
def get_from_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
def get_from_dashboard_for_station(station_id: int, start_date: date, end_date: date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
return api_database.get_dashboard_from(station_id, start_date, end_date)
|
return api_database.get_dashboard_from(station_id, start_date, end_date)
|
||||||
|
|
||||||
@ -54,9 +56,21 @@ class StationDashboardDurationGroup(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/duration", response_model=List[StationDashboardDurationGroup])
|
@router.get("/duration", response_model=List[StationDashboardDurationGroup])
|
||||||
def get_duration_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
def get_duration_dashboard_for_station(station_id: int, start_date: date, end_date: date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
return api_database.get_dashboard_duration(station_id, start_date, end_date)
|
db_data = api_database.get_dashboard_duration(station_id, start_date, end_date)
|
||||||
|
ret_val = []
|
||||||
|
for group in ['0-5', '5-15', '15-30', '30-45', '45+']:
|
||||||
|
curr_minute_group = list(filter(lambda x: x['minutesGroup'] == group, db_data))
|
||||||
|
if curr_minute_group:
|
||||||
|
item = curr_minute_group.pop()
|
||||||
|
ret_val.append({
|
||||||
|
'number': item['number'],
|
||||||
|
'minutesGroup': item['minutesGroup']
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
ret_val.append({'number': 0, 'minutesGroup': group})
|
||||||
|
return ret_val
|
||||||
|
|
||||||
|
|
||||||
class StationDashboardTimeGroup(BaseModel):
|
class StationDashboardTimeGroup(BaseModel):
|
||||||
@ -66,6 +80,18 @@ class StationDashboardTimeGroup(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/time", response_model=List[StationDashboardTimeGroup])
|
@router.get("/time", response_model=List[StationDashboardTimeGroup])
|
||||||
def get_time_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
def get_time_dashboard_for_station(station_id: int, start_date: date, end_date: date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
return api_database.get_dashboard_time(station_id, start_date, end_date)
|
db_data = api_database.get_dashboard_time(station_id, start_date, end_date)
|
||||||
|
|
||||||
|
ret_val = []
|
||||||
|
init_date = datetime.combine(date.today(), time(0, 0))
|
||||||
|
for i in range(144):
|
||||||
|
curr_interval = (init_date + timedelta(minutes=10 * i)).strftime("%H:%M")
|
||||||
|
search_interval = list(filter(lambda x: x['timeFrame'] == curr_interval, db_data))
|
||||||
|
if search_interval:
|
||||||
|
ret_val.append(search_interval.pop())
|
||||||
|
else:
|
||||||
|
ret_val.append({'timeFrame': curr_interval, 'number': 0, 'avgDuration': 0})
|
||||||
|
|
||||||
|
return ret_val
|
||||||
|
@ -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));
|