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/
## Projekt 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
```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.
To stop just shut down the container.
To stop just shut down the container.

View File

@ -13,7 +13,9 @@ app = FastAPI(
origins = [
"http://it-schwarz.net",
"https://it-schwarz.net"
"https://it-schwarz.net",
"http://localhost",
"http://localhost:4200",
]
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 dashboard d ON u.start_station_id = d.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):
query = """
SELECT
topPoints.*,
b.lat AS stationLat,
b.lon AS stationLon
FROM (
SELECT
u.end_station_name AS stationName,
u.end_station_id AS stationId,
count(*) AS number,
round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.end_station_name
ORDER BY number DESC
LIMIT 3
) as topPoints
JOIN bike_points b ON b.id_num = topPoints.stationId"""
u.start_station_name AS startStationName,
u.end_station_name AS endStationName,
count(*) AS number,
round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.start_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.end_station_name
ORDER BY number DESC
LIMIT 3"""
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()
def get_dashboard_from(station_id, start_date, end_date):
query = """
SELECT
topPoints.*,
b.lat AS stationLat,
b.lon AS stationLon
FROM (
SELECT
u.start_station_name AS stationName,
u.start_station_id AS stationId,
count(*) AS number,
round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.start_station_name
ORDER BY number DESC
LIMIT 3
) as topPoints
JOIN bike_points b ON b.id_num = topPoints.stationId"""
u.start_station_name AS startStationName,
u.end_station_name AS endStationName,
count(*) AS number,
round(avg(u.duration)) AS avgDuration
FROM usage_stats u
WHERE u.end_station_id = ? AND date(u.start_date, 'unixepoch') BETWEEN ? AND ?
GROUP BY u.start_station_name
ORDER BY number DESC
LIMIT 3"""
return get_db_connection().execute(query, (station_id, start_date, end_date)).fetchall()

View File

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

View File

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

View File

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

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