Compare commits
1 Commits
master
...
postgres-d
Author | SHA1 | Date | |
---|---|---|---|
36d5affbee |
@ -8,6 +8,5 @@ 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,7 +25,7 @@ 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 geovis-backend
|
docker run -v $(pwd):/app -p 8080:80 --restart always -d test
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
||||||
|
@ -13,7 +13,9 @@ 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,50 +37,36 @@ 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,)).fetchone()
|
return get_db_connection().execute(query, (station_id,)).fetchall()
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard_to(station_id, start_date, end_date):
|
def get_dashboard_to(station_id, start_date, end_date):
|
||||||
query = """
|
query = """
|
||||||
SELECT
|
SELECT
|
||||||
topPoints.*,
|
u.start_station_name AS startStationName,
|
||||||
b.lat AS stationLat,
|
u.end_station_name AS endStationName,
|
||||||
b.lon AS stationLon
|
count(*) AS number,
|
||||||
FROM (
|
round(avg(u.duration)) AS avgDuration
|
||||||
SELECT
|
FROM usage_stats u
|
||||||
u.end_station_name AS stationName,
|
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||||
u.end_station_id AS stationId,
|
GROUP BY u.end_station_name
|
||||||
count(*) AS number,
|
ORDER BY number DESC
|
||||||
round(avg(u.duration)) AS avgDuration
|
LIMIT 3"""
|
||||||
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
|
||||||
topPoints.*,
|
u.start_station_name AS startStationName,
|
||||||
b.lat AS stationLat,
|
u.end_station_name AS endStationName,
|
||||||
b.lon AS stationLon
|
count(*) AS number,
|
||||||
FROM (
|
round(avg(u.duration)) AS avgDuration
|
||||||
SELECT
|
FROM usage_stats u
|
||||||
u.start_station_name AS stationName,
|
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
|
||||||
u.start_station_id AS stationId,
|
GROUP BY u.start_station_name
|
||||||
count(*) AS number,
|
ORDER BY number DESC
|
||||||
round(avg(u.duration)) AS avgDuration
|
LIMIT 3"""
|
||||||
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,6 +2,8 @@ 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
|
||||||
|
|
||||||
@ -59,34 +61,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 = sqlite3.connect(DB_NAME, timeout=300)
|
conn = get_conn()
|
||||||
conn.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
cursor = conn.cursor()
|
||||||
rental_id INTEGER PRIMARY KEY,
|
cursor.execute("""CREATE TABLE IF NOT EXISTS usage_stats(
|
||||||
duration INTEGER,
|
rental_id BIGINT PRIMARY KEY,
|
||||||
bike_id INTEGER,
|
duration BIGINT,
|
||||||
end_date INTEGER,
|
bike_id BIGINT,
|
||||||
end_station_id INTEGER,
|
end_date TIMESTAMP,
|
||||||
|
end_station_id BIGINT,
|
||||||
end_station_name TEXT,
|
end_station_name TEXT,
|
||||||
start_date INTEGER,
|
start_date TIMESTAMP,
|
||||||
start_station_id INTEGER,
|
start_station_id BIGINT,
|
||||||
start_station_name TEXT
|
start_station_name TEXT
|
||||||
)""")
|
)""")
|
||||||
conn.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
|
cursor.execute("CREATE TABLE IF NOT EXISTS read_files(file_path TEXT, etag TEXT PRIMARY KEY)")
|
||||||
conn.execute("""CREATE TABLE IF NOT EXISTS bike_points(
|
cursor.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
|
||||||
)""")
|
)""")
|
||||||
conn.execute("""CREATE TABLE IF NOT EXISTS accidents(
|
cursor.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 TEXT,
|
date TIMESTAMP,
|
||||||
severity TEXT,
|
severity TEXT
|
||||||
UNIQUE (lat, lon, date)
|
|
||||||
)""")
|
)""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@ -132,20 +134,23 @@ def create_dashboard_table():
|
|||||||
|
|
||||||
def import_bikepoints():
|
def import_bikepoints():
|
||||||
LOG.info("Importing bikepoints")
|
LOG.info("Importing bikepoints")
|
||||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
conn = get_conn()
|
||||||
|
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")
|
||||||
conn.executemany("INSERT OR IGNORE INTO bike_points VALUES (?, ?, ?, ?, ?)", points)
|
cursor.executemany("INSERT INTO bike_points VALUES (%s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", 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(f"Importing accidents for year {year}")
|
LOG.info("Importing accidents")
|
||||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
conn = get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
def filter_pedal_cycles(accident):
|
def filter_pedal_cycles(accident):
|
||||||
for vehicle in accident['vehicles']:
|
for vehicle in accident['vehicles']:
|
||||||
@ -156,22 +161,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['lat'], a['lon'], a['location'], a['date'], a['severity']), accidents))
|
accidents = list(map(lambda a: (a['id'], 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")
|
||||||
conn.executemany("""INSERT OR IGNORE INTO
|
cursor.executemany("INSERT INTO accidents VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING", accidents)
|
||||||
accidents(lat, lon, location, date, severity)
|
|
||||||
VALUES (?, ?, ?, ?, ?)""", accidents)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
LOG.info(f"Accidents imported for year {year}")
|
LOG.info("Accidents importet")
|
||||||
|
|
||||||
|
|
||||||
def import_usage_stats_file(export_file: ApiExportFile):
|
def import_usage_stats_file(export_file: ApiExportFile):
|
||||||
conn = sqlite3.connect(DB_NAME, timeout=300)
|
conn = get_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
rows = conn.execute("SELECT * FROM read_files WHERE etag LIKE ?", (export_file.etag,)).fetchall()
|
cursor.execute("SELECT * FROM read_files WHERE etag = %s", (export_file.etag,))
|
||||||
if len(rows) != 0:
|
if len(cursor.fetchall()) != 0:
|
||||||
LOG.warning(f"Skipping import of {export_file.path}")
|
LOG.warning(f"Skipping import of {export_file.path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -191,13 +196,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
|
||||||
int(datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[3] else -1,
|
datetime.strptime(entry[3][:16], "%d/%m/%Y %H:%M") if entry[3] else None,
|
||||||
# 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
|
||||||
int(datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M").timestamp()) if entry[6] else -1,
|
datetime.strptime(entry[6][:16], "%d/%m/%Y %H:%M") if entry[6] else None,
|
||||||
# StartStation Id
|
# StartStation Id
|
||||||
int(entry[7]),
|
int(entry[7]),
|
||||||
# StartStation Name
|
# StartStation Name
|
||||||
@ -210,36 +215,45 @@ 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")
|
||||||
conn.executemany("INSERT OR IGNORE INTO usage_stats VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", mapped)
|
psycopg2.extras.execute_values(cursor, "INSERT INTO usage_stats VALUES %s ON CONFLICT DO NOTHING ", mapped, page_size=1_000_000)
|
||||||
conn.execute("INSERT OR IGNORE INTO read_files VALUES (?, ?)", (export_file.path, export_file.etag))
|
cursor.execute("INSERT INTO read_files VALUES (%s, %s) ON CONFLICT DO NOTHING", (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
|
||||||
for year in range(2005, 2020):
|
# import_accidents(2019)
|
||||||
import_accidents(year)
|
#
|
||||||
|
# if count_after - count_pre > 0:
|
||||||
if count_after - count_pre > 0:
|
# create_dashboard_table()
|
||||||
create_dashboard_table()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
@ -11,16 +10,10 @@ 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: Severity
|
severity: str
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime, time, timedelta
|
import datetime
|
||||||
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: date, end_date: date):
|
def validate_daterange(start_date: datetime.date, end_date: datetime.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,32 +20,30 @@ class StationDashboard(BaseModel):
|
|||||||
commonName: Optional[str]
|
commonName: Optional[str]
|
||||||
lat: Optional[float]
|
lat: Optional[float]
|
||||||
lon: Optional[float]
|
lon: Optional[float]
|
||||||
maxEndDate: Optional[date]
|
maxEndDate: Optional[datetime.date]
|
||||||
maxStartDate: Optional[date]
|
maxStartDate: Optional[datetime.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) or {}
|
return api_database.get_dashboard(station_id)[0]
|
||||||
|
|
||||||
|
|
||||||
class StationDashboardTopStationsEntry(BaseModel):
|
class StationDashboardTopStationsEntry(BaseModel):
|
||||||
stationName: str
|
startStationName: str
|
||||||
stationId: int
|
endStationName: str
|
||||||
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: date, end_date: date):
|
def get_to_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.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: date, end_date: date):
|
def get_from_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.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)
|
||||||
|
|
||||||
@ -56,21 +54,9 @@ 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: date, end_date: date):
|
def get_duration_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
db_data = api_database.get_dashboard_duration(station_id, start_date, end_date)
|
return 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):
|
||||||
@ -80,18 +66,6 @@ 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: date, end_date: date):
|
def get_time_dashboard_for_station(station_id: int, start_date: datetime.date, end_date: datetime.date):
|
||||||
validate_daterange(start_date, end_date)
|
validate_daterange(start_date, end_date)
|
||||||
db_data = api_database.get_dashboard_time(station_id, start_date, end_date)
|
return 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
|
|
||||||
|
75
projects/project-3/db/postgres.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- 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
|
@ -1,18 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,16 +0,0 @@
|
|||||||
# 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
@ -1,48 +0,0 @@
|
|||||||
# 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
|
|
@ -1,27 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,144 +0,0 @@
|
|||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
// @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
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,23 +0,0 @@
|
|||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
/* 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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
// 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
@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
<app-toolbar></app-toolbar>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
<app-footer></app-footer>
|
|
@ -1,35 +0,0 @@
|
|||||||
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!');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
<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>
|
|
@ -1,40 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,77 +0,0 @@
|
|||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
<mat-card class="p-0">
|
|
||||||
<div id="minimap" style="height: 30rem"></div>
|
|
||||||
</mat-card>
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,36 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
<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>
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,159 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
<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>
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,175 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
<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>
|
|
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,179 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
@ -1,4 +0,0 @@
|
|||||||
.header-image {
|
|
||||||
background-image: url('../../../assets/bike-point-blue.png');
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,223 +0,0 @@
|
|||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
<div class="footer d-flex justify-content-center align-items-center">
|
|
||||||
<div class="copyright">
|
|
||||||
<span>© Tim Herbst & Marcel Schwarz</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||||||
.footer {
|
|
||||||
height: 2vh;
|
|
||||||
background: #2f2f2f;
|
|
||||||
|
|
||||||
.copyright {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,15 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
<div>
|
|
||||||
<mat-slide-toggle [(ngModel)]="isFlagActive" (ngModelChange)="onChange($event)" color="warn">
|
|
||||||
auto refresh
|
|
||||||
</mat-slide-toggle>
|
|
||||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||||||
mat-slide-toggle {
|
|
||||||
margin-right: 1em;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,39 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<div class="map-container" fxLayout="row">
|
|
||||||
<div class="map-frame" fxFill>
|
|
||||||
<div id="map"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
#map {
|
|
||||||
height: 93vh;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,26 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
<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>
|
|
@ -1,14 +0,0 @@
|
|||||||
#route-button {
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-card {
|
|
||||||
width: 30em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#route-button {
|
|
||||||
padding: 0 10px;
|
|
||||||
background-color: #017bfe;
|
|
||||||
color: white;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,118 +0,0 @@
|
|||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,257 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
@ -1,4 +0,0 @@
|
|||||||
a:hover {
|
|
||||||
background: #086ed2;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,15 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
@ -1,4 +0,0 @@
|
|||||||
.button-wiki:hover {
|
|
||||||
background: #086ed2;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,15 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
@ -1,7 +0,0 @@
|
|||||||
.toolbar-spacer {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-toolbar {
|
|
||||||
height: 5vh;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,20 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 76 KiB |
@ -1,4 +0,0 @@
|
|||||||
export const environment = {
|
|
||||||
production: true,
|
|
||||||
apiUrl: 'https://it-schwarz.net/api/'
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
// 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.
|
|
Before Width: | Height: | Size: 15 KiB |
@ -1,15 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,12 +0,0 @@
|
|||||||
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));
|
|