Compare commits

..

1 Commits

Author SHA1 Message Date
36d5affbee Test 2020-12-21 19:17:20 +01:00
108 changed files with 175 additions and 33670 deletions

View File

@ -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

View File

@ -25,9 +25,9 @@ docker build -t geovis-backend .
``` ```
After the build make sure you are in the same directory as "bike-data.db" resides, if so, run After the build make sure you are in the same directory as "bike-data.db" resides, if so, run
```shell ```shell
docker run -v $(pwd):/app -p 8080:80 --restart always -d 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.
To stop just shut down the container. To stop just shut down the container.

View File

@ -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(

View File

@ -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()

View File

@ -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__":

View File

@ -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(

View File

@ -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

View 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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
}
}

View File

@ -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
}
}));
}
};

View File

@ -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));
});
});

View File

@ -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>;
}
}

View File

@ -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"
]
}
}

View File

@ -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
});
};

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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 {
}

View File

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

View File

@ -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!');
});
});

View File

@ -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');
}
}

View File

@ -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 {
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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)
);
}
}

View File

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

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
});
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

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

View File

@ -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();
});
});

View File

@ -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
});
}
}

View File

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

View File

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

View File

@ -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();
});
});

View File

@ -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 {
}
}

View File

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

View File

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

View File

@ -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();
});
});

View File

@ -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));
}
}

View File

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

View File

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

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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]);
}
}

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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
) {
}
}

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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>

View File

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

View File

@ -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();
});
});

View File

@ -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 {
}
}

View File

@ -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>

View File

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

View File

@ -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();
});
});

View File

@ -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 {
}
}

View File

@ -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>

View File

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

View File

@ -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();
});
});

View File

@ -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);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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>

View File

@ -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));

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