Merge branch 'frontend'

This commit is contained in:
Marcel Schwarz 2021-01-14 02:05:42 +01:00
commit 5bc58b8df9
100 changed files with 33535 additions and 0 deletions

View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

48
projects/project-3/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
**/*.css
**/*.css.map
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
**/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,27 @@
# Frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,144 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/frontend",
"allowedCommonJsDependencies": [
"apexcharts"
],
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss",
"src/theme.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "frontend:build"
},
"configurations": {
"production": {
"browserTarget": "frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images/",
"output": "./assets"
}
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss",
"src/theme.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "frontend:serve"
},
"configurations": {
"production": {
"devServerTarget": "frontend:serve:production"
}
}
}
}
}
},
"defaultProject": "frontend",
"cli": {
"analytics": false
}
}

View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('frontend app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/frontend'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

30056
projects/project-3/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.2.0",
"@angular/cdk": "^10.2.7",
"@angular/common": "~10.2.0",
"@angular/compiler": "~10.2.0",
"@angular/core": "~10.2.0",
"@angular/flex-layout": "^10.0.0-beta.32",
"@angular/forms": "~10.2.0",
"@angular/material": "^10.2.7",
"@angular/platform-browser": "~10.2.0",
"@angular/platform-browser-dynamic": "~10.2.0",
"@angular/router": "~10.2.0",
"apexcharts": "^3.23.0",
"bootstrap": "^4.5.3",
"jquery": "^3.5.1",
"leaflet": "~1.3.1",
"leaflet.heat": "^0.2.0",
"leaflet.markercluster": "^1.4.1",
"ng-apexcharts": "^1.5.6",
"rxjs": "~6.6.0",
"seconds-to-human-time": "^1.0.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.0",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
}
}

View File

@ -0,0 +1,17 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MapComponent} from './map/map.component';
import {DashboardComponent} from './dashboard/dashboard.component';
const routes: Routes = [
{path: '', redirectTo: 'map', pathMatch: 'full'},
{path: 'map', component: MapComponent},
{path: 'dashboard/:id', component: DashboardComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View File

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

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'frontend'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
});
});

View File

@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {Title} from '@angular/platform-browser';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public constructor(private title: Title) {
this.title.setTitle('Bike Stations in London');
}
}

View File

@ -0,0 +1,90 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {MapComponent} from './map/map.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatToolbarModule} from '@angular/material/toolbar';
import {FlexLayoutModule} from '@angular/flex-layout';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {HttpClientModule} from '@angular/common/http';
import {NgApexchartsModule} from 'ng-apexcharts';
import {DashboardComponent} from './dashboard/dashboard.component';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatCardModule} from '@angular/material/card';
import {MatMenuModule} from '@angular/material/menu';
import {LayoutModule} from '@angular/cdk/layout';
import {PopUpComponent} from './map/pop-up/pop-up.component';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatNativeDateModule} from '@angular/material/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatInputModule} from '@angular/material/input';
import {MatTableModule} from '@angular/material/table';
import {AutoRefreshComponent} from './map/auto-refresh/auto-refresh.component';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatTooltipModule} from '@angular/material/tooltip';
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import { TableComponent } from './dashboard/table/table.component';
import { RentDurationChartComponent } from './dashboard/rent-duration-chart/rent-duration-chart.component';
import { RentTimeChartComponent } from './dashboard/rent-time-chart/rent-time-chart.component';
import { UserInputComponent } from './dashboard/user-input/user-input.component';
import { MiniMapComponent } from './dashboard/mini-map/mini-map.component';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { MapInteractionComponent } from './toolbar/map-interaction/map-interaction.component';
import { DashboardInteractionComponent } from './toolbar/dashboard-interaction/dashboard-interaction.component';
import { FooterComponent } from './footer/footer.component';
@NgModule({
declarations: [
AppComponent,
MapComponent,
DashboardComponent,
PopUpComponent,
AutoRefreshComponent,
TableComponent,
RentDurationChartComponent,
RentTimeChartComponent,
UserInputComponent,
MiniMapComponent,
ToolbarComponent,
MapInteractionComponent,
DashboardInteractionComponent,
FooterComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
FlexLayoutModule,
HttpClientModule,
NgApexchartsModule,
MatGridListModule,
MatCardModule,
MatMenuModule,
LayoutModule,
MatSidenavModule,
MatDatepickerModule,
MatFormFieldModule,
MatNativeDateModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatTableModule,
MatSlideToggleModule,
MatCheckboxModule,
MatTooltipModule,
MatProgressSpinnerModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

@ -0,0 +1,24 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<div class="px-5 py-3" style="background: #2f2f2f">
<div>
<div class="row mb-3">
<app-user-input
(startEndDate)="onSubmit($event)"
class="col-xl-5 col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
</app-user-input>
<app-mini-map class="col-xl-7 col-lg-6 col-md-12"></app-mini-map>
</div>
<div class="mb-3">
<app-table></app-table>
</div>
<div class="row mb-3">
<app-rent-duration-chart class="col"></app-rent-duration-chart>
</div>
<div class="row mb-3">
<app-rent-time-chart class="col"></app-rent-time-chart>
</div>
</div>
</div>

View File

@ -0,0 +1,40 @@
import { LayoutModule } from '@angular/cdk/layout';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardComponent],
imports: [
NoopAnimationsModule,
LayoutModule,
MatButtonModule,
MatCardModule,
MatGridListModule,
MatIconModule,
MatMenuModule,
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
import {IDashboardCommonBikePoint} from '../service/domain/dashboard-common-bike-point';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
import {TableComponent} from './table/table.component';
import {RentDurationChartComponent} from './rent-duration-chart/rent-duration-chart.component';
import {RentTimeChartComponent} from './rent-time-chart/rent-time-chart.component';
import {StartEndDate} from './user-input/user-input.component';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartHeight = 460;
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
})
export class DashboardComponent implements OnInit {
@ViewChild(TableComponent) table: TableComponent;
@ViewChild(RentDurationChartComponent) durationChart: RentDurationChartComponent;
@ViewChild(RentTimeChartComponent) timeChart: RentTimeChartComponent;
constructor() {
}
ngOnInit(): void {
}
async onSubmit(startEndDate: StartEndDate): Promise<any> {
await this.table.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
await this.durationChart.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
await this.timeChart.onSubmit(
startEndDate.actualStartDate.toISOString().substring(0, 10),
startEndDate.actualEndDate.toISOString().substring(0, 10)
);
}
}

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MiniMapComponent } from './mini-map.component';
describe('MiniMapComponent', () => {
let component: MiniMapComponent;
let fixture: ComponentFixture<MiniMapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MiniMapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MiniMapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {MapService} from '../../service/map.service';
@Component({
selector: 'app-mini-map',
templateUrl: './mini-map.component.html',
styleUrls: ['./mini-map.component.scss']
})
export class MiniMapComponent implements OnInit {
bikePoint: IDashboardCommonBikePoint;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
private map: MapService
) { }
ngOnInit(): void {
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.initMap();
});
});
}
initMap(): void {
this.map.initDashboardMap(this.bikePoint.lat, this.bikePoint.lon, 17);
this.map.drawDashboardStationMarker(this.bikePoint);
}
}

View File

@ -0,0 +1,27 @@
<mat-card>
<mat-card-header>
<mat-card-title>Rental Duration</mat-card-title>
<mat-card-subtitle>
This chart shows the rent duration based on the currently selected station.
The time it takes for a rent which has the current station as origin is displayed here.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="!isLoading" class="station-dashboard-borrow-duration">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"></apx-chart>
</div>
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RentDurationChartComponent } from './rent-duration-chart.component';
describe('RentDurationChartComponent', () => {
let component: RentDurationChartComponent;
let fixture: ComponentFixture<RentDurationChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RentDurationChartComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RentDurationChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,159 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis,
ChartComponent
} from 'ng-apexcharts';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis;
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartType = 'duration';
@Component({
selector: 'app-rent-duration-chart',
templateUrl: './rent-duration-chart.component.html',
styleUrls: ['./rent-duration-chart.component.scss']
})
export class RentDurationChartComponent implements OnInit {
@ViewChild(ChartComponent) chart: ChartComponent;
chartOptions: Partial<ChartOptions>;
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoading: boolean;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
) {
this.chartOptions = {
series: [],
chart: {
type: 'bar'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.isLoading = true;
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initChart().catch(error => console.log(error));
});
});
}
async initChart(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
this.isLoading = false;
this.chartOptions = {
series: [
{
name: 'amount of drives',
data: source.map(value => value.number)
}
],
chart: {
type: 'bar',
height: '460',
toolbar: {
show: false
}
},
colors: ['#017bfe'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
endingShape: 'flat'
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
title: {
text: 'average rental duration'
},
categories: source.map(value => value.minutesGroup),
labels: {
formatter: value => {
return value + ' min';
}
}
},
yaxis: {
title: {
text: 'amount of drives'
}
},
noData: {
text: 'loading'
},
fill: {
opacity: 1
}
};
});
}
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoading = true;
this.service.fetchDashboardStationCharts(
this.bikePoint.id,
actualStartDate,
actualEndDate,
chartType
).then(source => {
this.isLoading = false;
setTimeout(() => {
this.chart.updateSeries([{
data: source.map(value => value.number)
}]);
}, 1000);
});
}
}

View File

@ -0,0 +1,30 @@
<mat-card>
<mat-card-header>
<mat-card-title>Rental Time</mat-card-title>
<mat-card-subtitle>
This chart shows the workload of the currently selected station in relation
of the time of the day. It is visualized at which time of the day a journey begins or ends (blue).
In addition, the average rental duration of the trips is displayed at the given time (green).
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
</div>
<div *ngIf="!isLoading" class="station-dashboard-borrow-time">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis">
</apx-chart>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RentTimeChartComponent } from './rent-time-chart.component';
describe('RentTimeChartComponent', () => {
let component: RentTimeChartComponent;
let fixture: ComponentFixture<RentTimeChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RentTimeChartComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RentTimeChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,175 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis,
ChartComponent
} from 'ng-apexcharts';
import {DashboardService} from '../../service/dashboard.service';
import {ActivatedRoute} from '@angular/router';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartType = 'time';
@Component({
selector: 'app-rent-time-chart',
templateUrl: './rent-time-chart.component.html',
styleUrls: ['./rent-time-chart.component.scss']
})
export class RentTimeChartComponent implements OnInit {
@ViewChild(ChartComponent) chart: ChartComponent;
chartOptions: Partial<ChartOptions>;
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoading: boolean;
constructor(
private route: ActivatedRoute,
private service: DashboardService
) {
this.chartOptions = {
series: [],
chart: {
type: 'line'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.isLoading = true;
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initChart().catch(error => console.log(error));
});
});
}
async initChart(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
this.isLoading = false;
this.chartOptions = {
series: [
{
name: 'amount of drives',
type: 'bar',
data: source.map(value => value.number)
},
{
name: 'average rental duration',
type: 'line',
data: source.map(value => Math.round(value.avgDuration / 60))
}
],
tooltip: {
enabled: true,
shared: true,
x: {
show: true
}
},
chart: {
toolbar: {
show: false
},
type: 'line',
height: '495',
zoom: {
enabled: true,
}
},
colors: ['#017bfe', '#51ca49'],
dataLabels: {
enabled: false,
},
stroke: {
curve: 'straight'
},
legend: {
show: true,
},
xaxis: {
title: {
text: 'time of the day'
},
categories: source.map(value => value.timeFrame),
tickAmount: 24,
tickPlacement: 'between'
},
yaxis: [{
title: {
text: 'amount of drives',
},
}, {
opposite: true,
title: {
text: 'average rental duration'
},
labels: {
formatter: (val: number): string => {
return val + ' min';
}
}
}],
fill: {
opacity: 1
}
};
});
}
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoading = true;
this.service.fetchDashboardStationCharts(
this.bikePoint.id,
actualStartDate,
actualEndDate,
chartType
).then(source => {
this.isLoading = false;
setTimeout(() => {
this.chart.updateSeries([{
data: source.map(value => value.number)
}, {
data: source.map(value => Math.round(value.avgDuration / 60))
}]);
}, 1000);
});
}
}

View File

@ -0,0 +1,110 @@
<div class="row">
<div class="col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
<mat-card>
<mat-card-header>
<mat-card-title>Top-3 rental destination</mat-card-title>
<mat-card-subtitle>
This table shows the top-3 destinations of rentals from this station by number of drives.
The Station can be sent to the map with the checkbox.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table [dataSource]="stationToSource" class="mat-elevation-z0 w-100" mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" class="p-3" mat-cell>
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
(click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
[disabled]="isCheckBoxDisable(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="endStationName">
<th *matHeaderCellDef mat-header-cell>Destination</th>
<td *matCellDef="let element" mat-cell>
<a [routerLink]="['/dashboard/', element.stationId]">{{element.stationName}}</a>
</td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell>Count</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell>Average duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell>Icon</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsTo" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsTo;" mat-row></tr>
</table>
<div *ngIf="isLoadingToSource" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="col-lg-6 col-md-12">
<mat-card>
<mat-card-header>
<mat-card-title>Top-3 rental origin</mat-card-title>
<mat-card-subtitle>
This table shows the top-3 origins of rentals to this station by number of drives.
The Station can be sent to the map with the checkbox.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<table [dataSource]="stationFromSource" class="mat-elevation-z0 w-100" mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" class="p-3" mat-cell>
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
(click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
[disabled]="isCheckBoxDisable(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="startStationName">
<th *matHeaderCellDef mat-header-cell>Origin</th>
<td *matCellDef="let element" mat-cell>
<a [routerLink]="['/dashboard/', element.stationId]"> {{element.stationName}}</a>
</td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell>Count</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell>Average duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell>Icon</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsFrom" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsFrom;" mat-row></tr>
</table>
<div *ngIf="isLoadingFromSource" class="col d-flex align-items-center justify-content-center">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,17 @@
img {
width: 60px;
}
a {
color: black;
text-decoration: underline;
}
.mat-checkbox-layout label {
margin: 0 !important;
}
.mat-cell, .mat-header-cell {
padding-left: 8px;
padding-right: 8px;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TableComponent } from './table.component';
describe('TableComponent', () => {
let component: TableComponent;
let fixture: ComponentFixture<TableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TableComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,179 @@
import {Component, OnInit} from '@angular/core';
import {MatTableDataSource} from '@angular/material/table';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {SelectionModel} from '@angular/cdk/collections';
import {MatCheckboxChange} from '@angular/material/checkbox';
import stht from 'seconds-to-human-time';
import {MapService} from '../../service/map.service';
import {DashboardService} from '../../service/dashboard.service';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss']
})
export class TableComponent implements OnInit {
displayedColumnsTo: string[] = ['select', 'endStationName', 'number', 'avgDuration', 'marker'];
displayedColumnsFrom: string[] = ['select', 'startStationName', 'number', 'avgDuration', 'marker'];
stationToSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableToSource: any[];
stationFromSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableFromSource: any[];
selectionModel = new SelectionModel<IDashboardCommonBikePoint>(true, []);
colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
isLoadingToSource = true;
isLoadingFromSource = true;
constructor(
private route: ActivatedRoute,
private map: MapService,
private service: DashboardService
) {
}
ngOnInit(): void {
this.route.params.subscribe(params => {
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initTable();
});
});
}
initTable(): void {
this.selectionModel.clear();
this.map.removeOverlayOnMiniMap();
const initDate = this.maxEndDate.toISOString().substring(0, 10);
this.loadData(initDate, initDate);
}
onSubmit(actualStartDate: string, actualEndDate: string): void {
this.resetTableSourcesToDisplaySpinner();
this.selectionModel.clear();
this.map.removeOverlayOnMiniMap();
this.loadData(actualStartDate, actualEndDate);
}
resetTableSourcesToDisplaySpinner(): void {
this.isLoadingToSource = true;
this.isLoadingFromSource = true;
this.stationToSource = null;
this.stationFromSource = null;
this.iterableToSource = [];
this.iterableFromSource = [];
}
async loadData(actualStartDate: string, actualEndDate: string): Promise<void> {
this.isLoadingToSource = true;
this.isLoadingFromSource = true;
const [stationTo, stationFrom] = await Promise.all([
this.service.fetchDashboardStationTo(this.bikePoint.id, actualStartDate, actualEndDate),
this.service.fetchDashboardStationFrom(this.bikePoint.id, actualStartDate, actualEndDate)
]);
this.isLoadingToSource = false;
this.isLoadingFromSource = false;
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
this.stationToSource = this.setBikePointColorToSource(stationTo);
this.iterableToSource = stationTo;
this.iterableToSource.forEach(bikePoint => bikePoint.polyLineColor = 'green');
this.stationFromSource = this.setBikePointColorFromSource(stationFrom);
this.iterableFromSource = stationFrom;
this.iterableFromSource.forEach(bikePoint => bikePoint.polyLineColor = 'red');
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

@ -0,0 +1,45 @@
<mat-card style="height: 30rem">
<mat-card-header>
<mat-card-title class="d-flex align-items-center">
<div class="header-image d-inline-flex" mat-card-avatar></div>
{{bikePoint?.commonName}}
</mat-card-title>
</mat-card-header>
<mat-card-content class="p-4 d-flex flex-column justify-content-center align-content-between">
<p>Select a range to analyze data</p>
<form [formGroup]="form" (submit)="onSubmit()">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Enter a range</mat-label>
<mat-date-range-input [max]="maxEndDate" [min]="maxStartDate" [rangePicker]="picker"
formGroupName="dateRange">
<input formControlName="start" matStartDate placeholder="Start date">
<input formControlName="end" matEndDate placeholder="End date">
</mat-date-range-input>
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<button (click)="onSubmit()" color="primary" type="submit" (keyup.enter)="onSubmit()" class="w-100" mat-raised-button>
reload
<mat-icon>cached</mat-icon>
</button>
</form>
<br/>
<div class="ml-n4" id="chart">
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[subtitle]="chartOptions.subtitle"
[title]="chartOptions.title"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"
></apx-chart>
</div>
</mat-card-content>
</mat-card>

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserInputComponent } from './user-input.component';
describe('UserInputComponent', () => {
let component: UserInputComponent;
let fixture: ComponentFixture<UserInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserInputComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UserInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,223 @@
import {Component, EventEmitter, Injectable, OnInit, Output} from '@angular/core';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
import {FormBuilder, FormControl, FormGroup} from '@angular/forms';
import {IMapBikePoint} from '../../service/domain/map-bike-point';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
import {DateAdapter, MAT_DATE_FORMATS, NativeDateAdapter} from '@angular/material/core';
import {formatDate} from '@angular/common';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis | ApexYAxis[];
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
export const PICK_FORMATS = {
parse: {dateInput: {month: 'short', year: 'numeric', day: 'numeric'}},
display: {
dateInput: 'input',
monthYearLabel: {year: 'numeric', month: 'numeric'},
dateA11yLabel: {year: 'numeric', month: 'numeric', day: 'numeric'},
monthYearA11yLabel: {year: 'numeric', month: 'long'}
}
};
@Injectable()
class PickDateAdapter extends NativeDateAdapter {
format(date: Date, displayFormat: Object): string {
if (displayFormat === 'input') {
return formatDate(date, 'dd.MM.yyyy', this.locale);
} else {
return date.toDateString();
}
}
}
export interface StartEndDate {
actualStartDate: Date;
actualEndDate: Date;
}
@Component({
selector: 'app-user-input',
templateUrl: './user-input.component.html',
styleUrls: ['./user-input.component.scss'],
providers: [
{provide: DateAdapter, useClass: PickDateAdapter},
{provide: MAT_DATE_FORMATS, useValue: PICK_FORMATS}
]
})
export class UserInputComponent implements OnInit {
@Output() startEndDate: EventEmitter<StartEndDate> = new EventEmitter<StartEndDate>();
chartOptions: Partial<ChartOptions>;
station: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
form: FormGroup;
bikePoint: IMapBikePoint;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
private fb: FormBuilder
) {
this.chartOptions = {
series: [],
chart: {
type: 'bar'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.form = this.fb.group({
dateRange: new FormGroup({
start: new FormControl(),
end: new FormControl()
})
});
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.station = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initInput().catch(error => console.log(error));
});
this.service.fetchBikePointForStatus(params.id).then(data => {
this.bikePoint = data;
const NbBlockedDocks = data.status.NbDocks - data.status.NbBikes - data.status.NbEmptyDocks;
this.chartOptions = {
subtitle: {
text: 'This chart visualizes the availability of the bikes',
offsetX: 20,
offsetY: 15,
style: {
fontSize: '15px'
}
},
series: [
{
name: 'Bikes',
data: [data.status.NbBikes]
},
{
name: 'Empty docks',
data: [data.status.NbEmptyDocks]
},
{
name: 'Blocked docks',
data: [NbBlockedDocks]
}
],
colors: ['#51ca49', '#8f8e8e', '#f00'],
chart: {
type: 'bar',
height: 180,
stacked: true,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: true,
dataLabels: {
position: 'center'
}
}
},
dataLabels: {
enabled: true,
style: {
fontSize: '20px',
colors: ['#fff']
}
},
stroke: {
show: false
},
xaxis: {
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
title: {
text: undefined
},
axisBorder: {
show: false
},
min: 0,
max: data.status.NbDocks
},
tooltip: {
enabled: false,
},
fill: {
opacity: 1
},
legend: {
position: 'bottom',
horizontalAlign: 'right',
fontSize: '14px'
}
};
});
});
}
async initInput(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
this.form.get('dateRange').get('start').setValue(initDate);
this.form.get('dateRange').get('end').setValue(initDate);
}
async onSubmit(): Promise<any> {
this.startEndDate.emit({
actualStartDate: this.form.get('dateRange').value.start,
actualEndDate: this.form.get('dateRange').value.end
});
}
}

View File

@ -0,0 +1,5 @@
<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

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AutoRefreshComponent } from './auto-refresh.component';
describe('AutoRefreshComponent', () => {
let component: AutoRefreshComponent;
let fixture: ComponentFixture<AutoRefreshComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AutoRefreshComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AutoRefreshComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {MapService} from '../../service/map.service';
import * as internal from 'events';
@Component({
selector: 'app-auto-refresh',
templateUrl: './auto-refresh.component.html',
styleUrls: ['./auto-refresh.component.scss']
})
export class AutoRefreshComponent implements OnInit, OnDestroy {
isFlagActive: boolean;
interval: internal;
constructor(private map: MapService) {
const storageFlag = JSON.parse(sessionStorage.getItem('auto-refresh'));
if (storageFlag) {
this.isFlagActive = storageFlag;
} else {
this.isFlagActive = false;
}
}
ngOnInit(): void {
this.interval = setInterval(() => {
if (this.isFlagActive) {
this.map.autoRefresh().catch(error => console.log(error));
}
}, 10000);
}
ngOnDestroy(): void {
clearInterval(this.interval);
}
onChange(flag: boolean): void {
sessionStorage.setItem('auto-refresh', JSON.stringify(flag));
}
}

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapComponent } from './map.component';
describe('MapComponent', () => {
let component: MapComponent;
let fixture: ComponentFixture<MapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import {Component, OnInit} from '@angular/core';
import {MapService} from '../service/map.service';
@Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements OnInit {
constructor(private service: MapService) {
}
ngOnInit(): void {
this.initMapView();
}
async initMapView(): Promise<any> {
this.service.initMap(51.509865, -0.118092, 14);
await this.service.drawStationMarkers();
this.service.drawHeatmap();
this.service.drawAccidents();
}
}

View File

@ -0,0 +1,25 @@
<mat-card class="mat-elevation-z0 p-0">
<mat-card-header>
<mat-card-title>{{station?.commonName}}</mat-card-title>
</mat-card-header>
<mat-card-content class="d-flex flex-column align-items-center justify-content-center">
<div id="chart" class="w-100 ml-n4">
<apx-chart
class="w-100"
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[title]="chartOptions.title"
[tooltip]="chartOptions.tooltip"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"
></apx-chart>
</div>
<button (click)="route()" mat-raised-button id="route-button">Open in dashboard</button>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,14 @@
#route-button {
margin: 10px 0 0 0;
padding: 0;
}
.mat-card {
width: 30em;
}
#route-button {
padding: 0 10px;
background-color: #017bfe;
color: white;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PopUpComponent } from './pop-up.component';
describe('PopUpComponent', () => {
let component: PopUpComponent;
let fixture: ComponentFixture<PopUpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PopUpComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PopUpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,118 @@
import {Component, OnInit} from '@angular/core';
import {IMapBikePoint} from '../../service/domain/map-bike-point';
import {Router} from '@angular/router';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis
} from 'ng-apexcharts';
export type ChartOptions = {
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
xaxis: ApexXAxis;
yaxis: ApexYAxis;
stroke: ApexStroke;
title: ApexTitleSubtitle;
tooltip: ApexTooltip;
fill: ApexFill;
legend: ApexLegend;
};
@Component({
selector: 'app-pop-up',
templateUrl: './pop-up.component.html',
styleUrls: ['./pop-up.component.scss']
})
export class PopUpComponent implements OnInit {
station: IMapBikePoint;
public chartOptions: Partial<ChartOptions>;
constructor(private router: Router) {
}
ngOnInit(): void {
const NbBlockedDocks = this.station.status.NbDocks - this.station.status.NbBikes - this.station.status.NbEmptyDocks;
this.chartOptions = {
series: [
{
name: 'Bikes',
data: [this.station.status.NbBikes]
},
{
name: 'Empty docks',
data: [this.station.status.NbEmptyDocks]
},
{
name: 'Blocked docks',
data: [NbBlockedDocks]
}
],
colors: ['#51ca49', '#8f8e8e', '#f00'],
chart: {
type: 'bar',
height: 125,
stacked: true,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
horizontal: true
}
},
stroke: {
show: false
},
xaxis: {
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
title: {
text: undefined
},
axisBorder: {
show: false
},
min: 0,
max: this.station.status.NbDocks
},
tooltip: {
enabled: false,
},
fill: {
opacity: 1
},
legend: {
position: 'bottom',
horizontalAlign: 'right'
}
};
}
public route(): void {
this.router.navigate(['/dashboard', this.station.id]);
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DashboardService } from './dashboard.service';
describe('DashboardService', () => {
let service: DashboardService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DashboardService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class DashboardService {
constructor(private client: HttpClient) {
}
public fetchDashboardInit(id: string): Promise<any> {
return this.client.get(environment.apiUrl + `latest/dashboard/${id}/`).toPromise();
}
public fetchBikePointForStatus(id: string): Promise<any> {
return this.client.get(environment.apiUrl + `latest/bikepoints/${id}/`).toPromise();
}
public fetchDashboardStationTo(id: string, startDate: string, endDate: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/to?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
public fetchDashboardStationFrom(id: string, startDate: string, endDate: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/from?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
public fetchDashboardStationCharts(id: string, startDate: string, endDate: string, type: string): Promise<any> {
return this.client.get(
environment.apiUrl + `latest/dashboard/${id}/${type}?start_date=${startDate}&end_date=${endDate}`
).toPromise();
}
}

View File

@ -0,0 +1,22 @@
export interface IDashboardCommonBikePoint {
id?: string;
color?: string;
commonName?: string;
lat?: number;
lon?: number;
maxEndDate?: string;
maxStartDate?: string;
}
export class DashboardCommonBikePoint implements IDashboardCommonBikePoint {
constructor(
public id?: string,
public color?: string,
public commonName?: string,
public lat?: number,
public lon?: number,
public maxEndDate?: string,
public maxStartDate?: string
) {
}
}

View File

@ -0,0 +1,24 @@
export interface IMapBikePoint {
id?: string;
commonName?: string;
lat?: number;
lon?: number;
status?: BikePointStatus;
}
export class MapBikePoint implements IMapBikePoint {
constructor(
public id?: string,
public commonName?: string,
public lat?: number,
public lon?: number,
public status?: BikePointStatus
) {
}
}
export class BikePointStatus {
NbBikes?: number;
NbEmptyDocks?: number;
NbDocks?: number;
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { MapService } from './map.service';
describe('MapService', () => {
let service: MapService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MapService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,256 @@
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: [1, -35]
});
@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
});
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

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { PopUpService } from './pop-up.service';
describe('PopUpService', () => {
let service: PopUpService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PopUpService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import {ComponentFactoryResolver, Injectable, Injector} from '@angular/core';
import {IMapBikePoint} from './domain/map-bike-point';
import {PopUpComponent} from '../map/pop-up/pop-up.component';
@Injectable({
providedIn: 'root'
})
export class PopUpService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {
}
makeAvailabilityPopUp(station: IMapBikePoint): any {
const factory = this.componentFactoryResolver.resolveComponentFactory(PopUpComponent);
const component = factory.create(this.injector);
component.instance.station = station;
component.changeDetectorRef.detectChanges();
return component.location.nativeElement;
}
}

View File

@ -0,0 +1,13 @@
<div class="d-flex">
<a color="primary"
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203"
mat-flat-button
target="_blank">
<mat-icon>library_books</mat-icon>
Wiki
</a>
<a color="primary" mat-flat-button routerLink="/">
<mat-icon>map</mat-icon>
back to map
</a>
</div>

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardInteractionComponent } from './dashboard-interaction.component';
describe('DashboardInteractionComponent', () => {
let component: DashboardInteractionComponent;
let fixture: ComponentFixture<DashboardInteractionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardInteractionComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardInteractionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dashboard-interaction',
templateUrl: './dashboard-interaction.component.html',
styleUrls: ['./dashboard-interaction.component.scss']
})
export class DashboardInteractionComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,9 @@
<div class="d-flex">
<app-auto-refresh></app-auto-refresh>
<a class="button-wiki" color="primary"
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203" mat-flat-button
target="_blank">
<mat-icon>library_books</mat-icon>
Wiki
</a>
</div>

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapInteractionComponent } from './map-interaction.component';
describe('InteractionComponent', () => {
let component: MapInteractionComponent;
let fixture: ComponentFixture<MapInteractionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapInteractionComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapInteractionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-map-interaction',
templateUrl: './map-interaction.component.html',
styleUrls: ['./map-interaction.component.scss']
})
export class MapInteractionComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,13 @@
<mat-toolbar class="mat-toolbar" color="primary">
<span routerLink="/" id="logo">
<img alt="" src="../../assets/Logo.png" height="40" class="mx-2 mb-1">
Bike Stations in London
</span>
<span class="toolbar-spacer"></span>
<div *ngIf="!hasRoute('dashboard')">
<app-map-interaction></app-map-interaction>
</div>
<div *ngIf="hasRoute('dashboard')">
<app-dashboard-interaction></app-dashboard-interaction>
</div>
</mat-toolbar>

View File

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

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarComponent } from './toolbar.component';
describe('ToolbarComponent', () => {
let component: ToolbarComponent;
let fixture: ComponentFixture<ToolbarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ToolbarComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ToolbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import {Router} from '@angular/router';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit(): void {
}
hasRoute(route: string): boolean {
return this.router.url.includes(route);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl: 'https://it-schwarz.net/api/'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1,77 @@
html, body {
height: 100vh;
min-width: 600px;
}
body {
margin: 0;
padding: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
@import "~leaflet/dist/leaflet.css";
@import "~leaflet.markercluster/dist/MarkerCluster.css";
@import "~leaflet.markercluster/dist/MarkerCluster.Default.css";
@import "~bootstrap/scss/bootstrap";
label.mat-checkbox-layout {
margin: 0;
}
#logo {
cursor: pointer;
outline: none;
}
.legend {
padding: 6px 8px;
font: 14px Arial, Helvetica, sans-serif;
background: white;
background: rgba(255, 255, 255, 0.8);
line-height: 24px;
color: #555;
h4 {
text-align: center;
font-size: 16px;
margin: 2px 12px 8px;
color: #777;
}
span {
position: relative;
bottom: 3px;
}
i {
width: 18px;
height: 3px;
float: left;
margin: 7px 8px 0 0;
opacity: 0.7;
.icon {
background-size: 18px;
background-color: rgba(255, 255, 255, 1);
}
}
}
.legend-accidents {
background: rgb(57, 57, 57);
color: white;
h4 {
color: white;
}
div {
display: flex;
justify-content: left;
align-items: baseline;
svg {
margin-right: 8px;
}
}
}

View File

@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,246 @@
/**
* Generated theme by Material Theme Generator
* https://materialtheme.arcsine.dev
*/
@import '~@angular/material/theming';
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Fonts
@import 'https://fonts.googleapis.com/css?family=Material+Icons';
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
$fontConfig: (
display-4: mat-typography-level(112px, 112px, 300, 'Roboto', -0.0134em),
display-3: mat-typography-level(56px, 56px, 400, 'Roboto', -0.0089em),
display-2: mat-typography-level(45px, 48px, 400, 'Roboto', 0.0000em),
display-1: mat-typography-level(34px, 40px, 400, 'Roboto', 0.0074em),
headline: mat-typography-level(24px, 32px, 400, 'Roboto', 0.0000em),
title: mat-typography-level(20px, 32px, 500, 'Roboto', 0.0075em),
subheading-2: mat-typography-level(16px, 28px, 400, 'Roboto', 0.0094em),
subheading-1: mat-typography-level(15px, 24px, 500, 'Roboto', 0.0067em),
body-2: mat-typography-level(14px, 24px, 500, 'Roboto', 0.0179em),
body-1: mat-typography-level(14px, 20px, 400, 'Roboto', 0.0179em),
button: mat-typography-level(14px, 14px, 500, 'Roboto', 0.0893em),
caption: mat-typography-level(12px, 20px, 400, 'Roboto', 0.0333em),
input: mat-typography-level(inherit, 1.125, 400, 'Roboto', 1.5px)
);
// Foreground Elements
// Light Theme Text
$dark-text: #000000;
$dark-primary-text: rgba($dark-text, 0.87);
$dark-accent-text: rgba($dark-primary-text, 0.54);
$dark-disabled-text: rgba($dark-primary-text, 0.38);
$dark-dividers: rgba($dark-primary-text, 0.12);
$dark-focused: rgba($dark-primary-text, 0.12);
$mat-light-theme-foreground: (
base: black,
divider: $dark-dividers,
dividers: $dark-dividers,
disabled: $dark-disabled-text,
disabled-button: rgba($dark-text, 0.26),
disabled-text: $dark-disabled-text,
elevation: black,
secondary-text: $dark-accent-text,
hint-text: $dark-disabled-text,
accent-text: $dark-accent-text,
icon: $dark-accent-text,
icons: $dark-accent-text,
text: $dark-primary-text,
slider-min: $dark-primary-text,
slider-off: rgba($dark-text, 0.26),
slider-off-active: $dark-disabled-text,
);
// Dark Theme text
$light-text: #ffffff;
$light-primary-text: $light-text;
$light-accent-text: rgba($light-primary-text, 0.7);
$light-disabled-text: rgba($light-primary-text, 0.5);
$light-dividers: rgba($light-primary-text, 0.12);
$light-focused: rgba($light-primary-text, 0.12);
$mat-dark-theme-foreground: (
base: $light-text,
divider: $light-dividers,
dividers: $light-dividers,
disabled: $light-disabled-text,
disabled-button: rgba($light-text, 0.3),
disabled-text: $light-disabled-text,
elevation: black,
hint-text: $light-disabled-text,
secondary-text: $light-accent-text,
accent-text: $light-accent-text,
icon: $light-text,
icons: $light-text,
text: $light-text,
slider-min: $light-text,
slider-off: rgba($light-text, 0.3),
slider-off-active: rgba($light-text, 0.3),
);
// Background config
// Light bg
$light-background: #fafafa;
$light-bg-darker-5: darken($light-background, 5%);
$light-bg-darker-10: darken($light-background, 10%);
$light-bg-darker-20: darken($light-background, 20%);
$light-bg-darker-30: darken($light-background, 30%);
$light-bg-lighter-5: lighten($light-background, 5%);
$dark-bg-tooltip: lighten(#2c2c2c, 20%);
$dark-bg-alpha-4: rgba(#2c2c2c, 0.04);
$dark-bg-alpha-12: rgba(#2c2c2c, 0.12);
$mat-light-theme-background: (
background: $light-background,
status-bar: $light-bg-darker-20,
app-bar: $light-bg-darker-5,
hover: $dark-bg-alpha-4,
card: $light-bg-lighter-5,
dialog: $light-bg-lighter-5,
tooltip: $dark-bg-tooltip,
disabled-button: $dark-bg-alpha-12,
raised-button: $light-bg-lighter-5,
focused-button: $dark-focused,
selected-button: $light-bg-darker-20,
selected-disabled-button: $light-bg-darker-30,
disabled-button-toggle: $light-bg-darker-10,
unselected-chip: $light-bg-darker-10,
disabled-list-option: $light-bg-darker-10,
);
// Dark bg
$dark-background: #2c2c2c;
$dark-bg-lighter-5: lighten($dark-background, 5%);
$dark-bg-lighter-10: lighten($dark-background, 10%);
$dark-bg-lighter-20: lighten($dark-background, 20%);
$dark-bg-lighter-30: lighten($dark-background, 30%);
$light-bg-alpha-4: rgba(#fafafa, 0.04);
$light-bg-alpha-12: rgba(#fafafa, 0.12);
// Background palette for dark themes.
$mat-dark-theme-background: (
background: $dark-background,
status-bar: $dark-bg-lighter-20,
app-bar: $dark-bg-lighter-5,
hover: $light-bg-alpha-4,
card: $dark-bg-lighter-5,
dialog: $dark-bg-lighter-5,
tooltip: $dark-bg-lighter-20,
disabled-button: $light-bg-alpha-12,
raised-button: $dark-bg-lighter-5,
focused-button: $light-focused,
selected-button: $dark-bg-lighter-20,
selected-disabled-button: $dark-bg-lighter-30,
disabled-button-toggle: $dark-bg-lighter-10,
unselected-chip: $dark-bg-lighter-20,
disabled-list-option: $dark-bg-lighter-10,
);
// Compute font config
@include mat-core($fontConfig);
// Theme Config
body {
--primary-color: #017bfe;
--primary-lighter-color: #b3d7ff;
--primary-darker-color: #015efe;
--text-primary-color: #{$light-primary-text};
--text-primary-lighter-color: #{$dark-primary-text};
--text-primary-darker-color: #{$light-primary-text};
}
$mat-primary: (
main: #017bfe,
lighter: #b3d7ff,
darker: #015efe,
200: #017bfe, // For slide toggle,
contrast : (
main: $light-primary-text,
lighter: $dark-primary-text,
darker: $light-primary-text,
)
);
$theme-primary: mat-palette($mat-primary, main, lighter, darker);
body {
--accent-color: #797979;
--accent-lighter-color: #d7d7d7;
--accent-darker-color: #5c5c5c;
--text-accent-color: #{$light-primary-text};
--text-accent-lighter-color: #{$dark-primary-text};
--text-accent-darker-color: #{$light-primary-text};
}
$mat-accent: (
main: #797979,
lighter: #d7d7d7,
darker: #5c5c5c,
200: #797979, // For slide toggle,
contrast : (
main: $light-primary-text,
lighter: $dark-primary-text,
darker: $light-primary-text,
)
);
$theme-accent: mat-palette($mat-accent, main, lighter, darker);
body {
--warn-color: #ff0000;
--warn-lighter-color: #ffb3b3;
--warn-darker-color: #ff0000;
--text-warn-color: #{$light-primary-text};
--text-warn-lighter-color: #{$dark-primary-text};
--text-warn-darker-color: #{$light-primary-text};
}
$mat-warn: (
main: #ff0000,
lighter: #ffb3b3,
darker: #ff0000,
200: #ff0000, // For slide toggle,
contrast : (
main: $light-primary-text,
lighter: $dark-primary-text,
darker: $light-primary-text,
)
);
$theme-warn: mat-palette($mat-warn, main, lighter, darker);;
$theme: mat-light-theme($theme-primary, $theme-accent, $theme-warn);
$altTheme: mat-dark-theme($theme-primary, $theme-accent, $theme-warn);
// Theme Init
@include angular-material-theme($theme);
.theme-alternate {
@include angular-material-theme($altTheme);
}
// Specific component overrides, pieces that are not in line with the general theming
// Handle buttons appropriately, with respect to line-height
.mat-raised-button, .mat-stroked-button, .mat-flat-button {
padding: 0 1.15em;
margin: 0 .65em;
min-width: 3em;
line-height: 36.4px
}
.mat-standard-chip {
padding: .5em .85em;
min-height: 2.5em;
}
.material-icons {
font-size: 24px;
font-family: 'Material Icons', 'Material Icons';
.mat-badge-content {
font-family: 'Roboto';
}
}

View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,20 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
}
}

View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,152 @@
{
"extends": "tslint:recommended",
"rulesDirectory": [
"codelyzer"
],
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}