Merge branch 'frontend'
18
projects/project-3/frontend/.browserslistrc
Normal file
@ -0,0 +1,18 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
16
projects/project-3/frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
48
projects/project-3/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
**/*.css
|
||||
**/*.css.map
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
**/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
27
projects/project-3/frontend/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Frontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
144
projects/project-3/frontend/angular.json
Normal file
@ -0,0 +1,144 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"allowedCommonJsDependencies": [
|
||||
"apexcharts"
|
||||
],
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
|
||||
"src/styles.scss",
|
||||
"src/theme.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "frontend:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/leaflet/dist/images/",
|
||||
"output": "./assets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
|
||||
"src/styles.scss",
|
||||
"src/theme.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "frontend:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "frontend:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "frontend",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
36
projects/project-3/frontend/e2e/protractor.conf.js
Normal file
@ -0,0 +1,36 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
23
projects/project-3/frontend/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('frontend app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
11
projects/project-3/frontend/e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl) as Promise<unknown>;
|
||||
}
|
||||
|
||||
getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
14
projects/project-3/frontend/e2e/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
32
projects/project-3/frontend/karma.conf.js
Normal file
@ -0,0 +1,32 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/frontend'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
30056
projects/project-3/frontend/package-lock.json
generated
Normal file
57
projects/project-3/frontend/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build --prod",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~10.2.0",
|
||||
"@angular/cdk": "^10.2.7",
|
||||
"@angular/common": "~10.2.0",
|
||||
"@angular/compiler": "~10.2.0",
|
||||
"@angular/core": "~10.2.0",
|
||||
"@angular/flex-layout": "^10.0.0-beta.32",
|
||||
"@angular/forms": "~10.2.0",
|
||||
"@angular/material": "^10.2.7",
|
||||
"@angular/platform-browser": "~10.2.0",
|
||||
"@angular/platform-browser-dynamic": "~10.2.0",
|
||||
"@angular/router": "~10.2.0",
|
||||
"apexcharts": "^3.23.0",
|
||||
"bootstrap": "^4.5.3",
|
||||
"jquery": "^3.5.1",
|
||||
"leaflet": "~1.3.1",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"ng-apexcharts": "^1.5.6",
|
||||
"rxjs": "~6.6.0",
|
||||
"seconds-to-human-time": "^1.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1002.0",
|
||||
"@angular/cli": "~10.2.0",
|
||||
"@angular/compiler-cli": "~10.2.0",
|
||||
"@types/jasmine": "~3.5.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.2"
|
||||
}
|
||||
}
|
17
projects/project-3/frontend/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {MapComponent} from './map/map.component';
|
||||
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', redirectTo: 'map', pathMatch: 'full'},
|
||||
{path: 'map', component: MapComponent},
|
||||
{path: 'dashboard/:id', component: DashboardComponent}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
}
|
3
projects/project-3/frontend/src/app/app.component.html
Normal file
@ -0,0 +1,3 @@
|
||||
<app-toolbar></app-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
35
projects/project-3/frontend/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'frontend'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
|
||||
});
|
||||
});
|
14
projects/project-3/frontend/src/app/app.component.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
public constructor(private title: Title) {
|
||||
this.title.setTitle('Bike Stations in London');
|
||||
}
|
||||
}
|
90
projects/project-3/frontend/src/app/app.module.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {MapComponent} from './map/map.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatToolbarModule} from '@angular/material/toolbar';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {NgApexchartsModule} from 'ng-apexcharts';
|
||||
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||
import {MatGridListModule} from '@angular/material/grid-list';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {LayoutModule} from '@angular/cdk/layout';
|
||||
import {PopUpComponent} from './map/pop-up/pop-up.component';
|
||||
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||
import {MatDatepickerModule} from '@angular/material/datepicker';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatNativeDateModule} from '@angular/material/core';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {AutoRefreshComponent} from './map/auto-refresh/auto-refresh.component';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import { TableComponent } from './dashboard/table/table.component';
|
||||
import { RentDurationChartComponent } from './dashboard/rent-duration-chart/rent-duration-chart.component';
|
||||
import { RentTimeChartComponent } from './dashboard/rent-time-chart/rent-time-chart.component';
|
||||
import { UserInputComponent } from './dashboard/user-input/user-input.component';
|
||||
import { MiniMapComponent } from './dashboard/mini-map/mini-map.component';
|
||||
import { ToolbarComponent } from './toolbar/toolbar.component';
|
||||
import { MapInteractionComponent } from './toolbar/map-interaction/map-interaction.component';
|
||||
import { DashboardInteractionComponent } from './toolbar/dashboard-interaction/dashboard-interaction.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
MapComponent,
|
||||
DashboardComponent,
|
||||
PopUpComponent,
|
||||
AutoRefreshComponent,
|
||||
TableComponent,
|
||||
RentDurationChartComponent,
|
||||
RentTimeChartComponent,
|
||||
UserInputComponent,
|
||||
MiniMapComponent,
|
||||
ToolbarComponent,
|
||||
MapInteractionComponent,
|
||||
DashboardInteractionComponent,
|
||||
FooterComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
FlexLayoutModule,
|
||||
HttpClientModule,
|
||||
NgApexchartsModule,
|
||||
MatGridListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
LayoutModule,
|
||||
MatSidenavModule,
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatNativeDateModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
MatSlideToggleModule,
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule,
|
||||
MatProgressSpinnerModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<div class="px-5 py-3" style="background: #2f2f2f">
|
||||
<div>
|
||||
<div class="row mb-3">
|
||||
<app-user-input
|
||||
(startEndDate)="onSubmit($event)"
|
||||
class="col-xl-5 col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
|
||||
</app-user-input>
|
||||
<app-mini-map class="col-xl-7 col-lg-6 col-md-12"></app-mini-map>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-table></app-table>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<app-rent-duration-chart class="col"></app-rent-duration-chart>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<app-rent-time-chart class="col"></app-rent-time-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { LayoutModule } from '@angular/cdk/layout';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DashboardComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
LayoutModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatGridListModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,77 @@
|
||||
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {IDashboardCommonBikePoint} from '../service/domain/dashboard-common-bike-point';
|
||||
|
||||
|
||||
import {
|
||||
ApexAxisChartSeries,
|
||||
ApexChart,
|
||||
ApexDataLabels,
|
||||
ApexFill,
|
||||
ApexLegend,
|
||||
ApexNoData,
|
||||
ApexPlotOptions,
|
||||
ApexStroke,
|
||||
ApexTitleSubtitle,
|
||||
ApexTooltip,
|
||||
ApexXAxis,
|
||||
ApexYAxis
|
||||
} from 'ng-apexcharts';
|
||||
import {TableComponent} from './table/table.component';
|
||||
import {RentDurationChartComponent} from './rent-duration-chart/rent-duration-chart.component';
|
||||
import {RentTimeChartComponent} from './rent-time-chart/rent-time-chart.component';
|
||||
import {StartEndDate} from './user-input/user-input.component';
|
||||
|
||||
export type ChartOptions = {
|
||||
title: ApexTitleSubtitle;
|
||||
subtitle: ApexTitleSubtitle;
|
||||
series: ApexAxisChartSeries;
|
||||
chart: ApexChart;
|
||||
colors: string[];
|
||||
dataLabels: ApexDataLabels;
|
||||
plotOptions: ApexPlotOptions;
|
||||
yaxis: ApexYAxis | ApexYAxis[];
|
||||
xaxis: ApexXAxis;
|
||||
fill: ApexFill;
|
||||
tooltip: ApexTooltip;
|
||||
stroke: ApexStroke;
|
||||
legend: ApexLegend;
|
||||
noData: ApexNoData;
|
||||
};
|
||||
|
||||
const chartHeight = 460;
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
@ViewChild(TableComponent) table: TableComponent;
|
||||
@ViewChild(RentDurationChartComponent) durationChart: RentDurationChartComponent;
|
||||
@ViewChild(RentTimeChartComponent) timeChart: RentTimeChartComponent;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
async onSubmit(startEndDate: StartEndDate): Promise<any> {
|
||||
await this.table.onSubmit(
|
||||
startEndDate.actualStartDate.toISOString().substring(0, 10),
|
||||
startEndDate.actualEndDate.toISOString().substring(0, 10)
|
||||
);
|
||||
await this.durationChart.onSubmit(
|
||||
startEndDate.actualStartDate.toISOString().substring(0, 10),
|
||||
startEndDate.actualEndDate.toISOString().substring(0, 10)
|
||||
);
|
||||
await this.timeChart.onSubmit(
|
||||
startEndDate.actualStartDate.toISOString().substring(0, 10),
|
||||
startEndDate.actualEndDate.toISOString().substring(0, 10)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<mat-card class="p-0">
|
||||
<div id="minimap" style="height: 30rem"></div>
|
||||
</mat-card>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MiniMapComponent } from './mini-map.component';
|
||||
|
||||
describe('MiniMapComponent', () => {
|
||||
let component: MiniMapComponent;
|
||||
let fixture: ComponentFixture<MiniMapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MiniMapComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MiniMapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {DashboardService} from '../../service/dashboard.service';
|
||||
import {MapService} from '../../service/map.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mini-map',
|
||||
templateUrl: './mini-map.component.html',
|
||||
styleUrls: ['./mini-map.component.scss']
|
||||
})
|
||||
export class MiniMapComponent implements OnInit {
|
||||
|
||||
bikePoint: IDashboardCommonBikePoint;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private service: DashboardService,
|
||||
private map: MapService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.subscribe(params => {
|
||||
this.service.fetchDashboardInit(params.id).then(data => {
|
||||
this.bikePoint = data;
|
||||
this.initMap();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initMap(): void {
|
||||
this.map.initDashboardMap(this.bikePoint.lat, this.bikePoint.lon, 17);
|
||||
this.map.drawDashboardStationMarker(this.bikePoint);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Rental Duration</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
This chart shows the rent duration based on the currently selected station.
|
||||
The time it takes for a rent which has the current station as origin is displayed here.
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngIf="!isLoading" class="station-dashboard-borrow-duration">
|
||||
<apx-chart
|
||||
[chart]="chartOptions.chart"
|
||||
[colors]="chartOptions.colors"
|
||||
[dataLabels]="chartOptions.dataLabels"
|
||||
[fill]="chartOptions.fill"
|
||||
[legend]="chartOptions.legend"
|
||||
[plotOptions]="chartOptions.plotOptions"
|
||||
[series]="chartOptions.series"
|
||||
[stroke]="chartOptions.stroke"
|
||||
[xaxis]="chartOptions.xaxis"
|
||||
[yaxis]="chartOptions.yaxis"></apx-chart>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
|
||||
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RentDurationChartComponent } from './rent-duration-chart.component';
|
||||
|
||||
describe('RentDurationChartComponent', () => {
|
||||
let component: RentDurationChartComponent;
|
||||
let fixture: ComponentFixture<RentDurationChartComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ RentDurationChartComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RentDurationChartComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,159 @@
|
||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {
|
||||
ApexAxisChartSeries,
|
||||
ApexChart,
|
||||
ApexDataLabels,
|
||||
ApexFill,
|
||||
ApexLegend,
|
||||
ApexNoData,
|
||||
ApexPlotOptions,
|
||||
ApexStroke,
|
||||
ApexTitleSubtitle,
|
||||
ApexTooltip,
|
||||
ApexXAxis,
|
||||
ApexYAxis,
|
||||
ChartComponent
|
||||
} from 'ng-apexcharts';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {DashboardService} from '../../service/dashboard.service';
|
||||
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
|
||||
|
||||
export type ChartOptions = {
|
||||
title: ApexTitleSubtitle;
|
||||
subtitle: ApexTitleSubtitle;
|
||||
series: ApexAxisChartSeries;
|
||||
chart: ApexChart;
|
||||
colors: string[];
|
||||
dataLabels: ApexDataLabels;
|
||||
plotOptions: ApexPlotOptions;
|
||||
yaxis: ApexYAxis;
|
||||
xaxis: ApexXAxis;
|
||||
fill: ApexFill;
|
||||
tooltip: ApexTooltip;
|
||||
stroke: ApexStroke;
|
||||
legend: ApexLegend;
|
||||
noData: ApexNoData;
|
||||
};
|
||||
|
||||
const chartType = 'duration';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-duration-chart',
|
||||
templateUrl: './rent-duration-chart.component.html',
|
||||
styleUrls: ['./rent-duration-chart.component.scss']
|
||||
})
|
||||
export class RentDurationChartComponent implements OnInit {
|
||||
|
||||
@ViewChild(ChartComponent) chart: ChartComponent;
|
||||
chartOptions: Partial<ChartOptions>;
|
||||
|
||||
bikePoint: IDashboardCommonBikePoint;
|
||||
maxStartDate: Date;
|
||||
maxEndDate: Date;
|
||||
isLoading: boolean;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private service: DashboardService,
|
||||
) {
|
||||
this.chartOptions = {
|
||||
series: [],
|
||||
chart: {
|
||||
type: 'bar'
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true;
|
||||
this.route.params.subscribe(params => {
|
||||
this.service.fetchDashboardInit(params.id).then(data => {
|
||||
this.bikePoint = data;
|
||||
this.maxStartDate = new Date(data.maxStartDate);
|
||||
this.maxEndDate = new Date(data.maxEndDate);
|
||||
this.initChart().catch(error => console.log(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async initChart(): Promise<void> {
|
||||
const initDate = this.maxEndDate.toISOString().substring(0, 10);
|
||||
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
|
||||
this.isLoading = false;
|
||||
this.chartOptions = {
|
||||
series: [
|
||||
{
|
||||
name: 'amount of drives',
|
||||
data: source.map(value => value.number)
|
||||
}
|
||||
],
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: '460',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
colors: ['#017bfe'],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '55%',
|
||||
endingShape: 'flat'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 2,
|
||||
colors: ['transparent']
|
||||
},
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'average rental duration'
|
||||
},
|
||||
categories: source.map(value => value.minutesGroup),
|
||||
labels: {
|
||||
formatter: value => {
|
||||
return value + ' min';
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'amount of drives'
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'loading'
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
|
||||
this.isLoading = true;
|
||||
this.service.fetchDashboardStationCharts(
|
||||
this.bikePoint.id,
|
||||
actualStartDate,
|
||||
actualEndDate,
|
||||
chartType
|
||||
).then(source => {
|
||||
this.isLoading = false;
|
||||
setTimeout(() => {
|
||||
this.chart.updateSeries([{
|
||||
data: source.map(value => value.number)
|
||||
}]);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Rental Time</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
This chart shows the workload of the currently selected station in relation
|
||||
of the time of the day. It is visualized at which time of the day a journey begins or ends (blue).
|
||||
In addition, the average rental duration of the trips is displayed at the given time (green).
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<div *ngIf="isLoading" class="col d-flex align-items-center justify-content-center">
|
||||
<mat-progress-spinner color="primary" mode="indeterminate" [diameter]="300"></mat-progress-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="station-dashboard-borrow-time">
|
||||
<apx-chart
|
||||
[chart]="chartOptions.chart"
|
||||
[colors]="chartOptions.colors"
|
||||
[dataLabels]="chartOptions.dataLabels"
|
||||
[fill]="chartOptions.fill"
|
||||
[legend]="chartOptions.legend"
|
||||
[series]="chartOptions.series"
|
||||
[stroke]="chartOptions.stroke"
|
||||
[tooltip]="chartOptions.tooltip"
|
||||
[xaxis]="chartOptions.xaxis"
|
||||
[yaxis]="chartOptions.yaxis">
|
||||
</apx-chart>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RentTimeChartComponent } from './rent-time-chart.component';
|
||||
|
||||
describe('RentTimeChartComponent', () => {
|
||||
let component: RentTimeChartComponent;
|
||||
let fixture: ComponentFixture<RentTimeChartComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ RentTimeChartComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RentTimeChartComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,175 @@
|
||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {
|
||||
ApexAxisChartSeries,
|
||||
ApexChart,
|
||||
ApexDataLabels,
|
||||
ApexFill,
|
||||
ApexLegend,
|
||||
ApexNoData,
|
||||
ApexPlotOptions,
|
||||
ApexStroke,
|
||||
ApexTitleSubtitle,
|
||||
ApexTooltip,
|
||||
ApexXAxis,
|
||||
ApexYAxis,
|
||||
ChartComponent
|
||||
} from 'ng-apexcharts';
|
||||
import {DashboardService} from '../../service/dashboard.service';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
|
||||
|
||||
export type ChartOptions = {
|
||||
title: ApexTitleSubtitle;
|
||||
subtitle: ApexTitleSubtitle;
|
||||
series: ApexAxisChartSeries;
|
||||
chart: ApexChart;
|
||||
colors: string[];
|
||||
dataLabels: ApexDataLabels;
|
||||
plotOptions: ApexPlotOptions;
|
||||
yaxis: ApexYAxis | ApexYAxis[];
|
||||
xaxis: ApexXAxis;
|
||||
fill: ApexFill;
|
||||
tooltip: ApexTooltip;
|
||||
stroke: ApexStroke;
|
||||
legend: ApexLegend;
|
||||
noData: ApexNoData;
|
||||
};
|
||||
|
||||
const chartType = 'time';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-time-chart',
|
||||
templateUrl: './rent-time-chart.component.html',
|
||||
styleUrls: ['./rent-time-chart.component.scss']
|
||||
})
|
||||
export class RentTimeChartComponent implements OnInit {
|
||||
|
||||
@ViewChild(ChartComponent) chart: ChartComponent;
|
||||
chartOptions: Partial<ChartOptions>;
|
||||
|
||||
bikePoint: IDashboardCommonBikePoint;
|
||||
maxStartDate: Date;
|
||||
maxEndDate: Date;
|
||||
isLoading: boolean;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private service: DashboardService
|
||||
) {
|
||||
this.chartOptions = {
|
||||
series: [],
|
||||
chart: {
|
||||
type: 'line'
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true;
|
||||
this.route.params.subscribe(params => {
|
||||
this.service.fetchDashboardInit(params.id).then(data => {
|
||||
this.bikePoint = data;
|
||||
this.maxStartDate = new Date(data.maxStartDate);
|
||||
this.maxEndDate = new Date(data.maxEndDate);
|
||||
this.initChart().catch(error => console.log(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async initChart(): Promise<void> {
|
||||
const initDate = this.maxEndDate.toISOString().substring(0, 10);
|
||||
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
|
||||
this.isLoading = false;
|
||||
this.chartOptions = {
|
||||
series: [
|
||||
{
|
||||
name: 'amount of drives',
|
||||
type: 'bar',
|
||||
data: source.map(value => value.number)
|
||||
},
|
||||
{
|
||||
name: 'average rental duration',
|
||||
type: 'line',
|
||||
data: source.map(value => Math.round(value.avgDuration / 60))
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
shared: true,
|
||||
x: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
type: 'line',
|
||||
height: '495',
|
||||
zoom: {
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
colors: ['#017bfe', '#51ca49'],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight'
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
},
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'time of the day'
|
||||
},
|
||||
categories: source.map(value => value.timeFrame),
|
||||
tickAmount: 24,
|
||||
tickPlacement: 'between'
|
||||
},
|
||||
yaxis: [{
|
||||
title: {
|
||||
text: 'amount of drives',
|
||||
},
|
||||
}, {
|
||||
opposite: true,
|
||||
title: {
|
||||
text: 'average rental duration'
|
||||
},
|
||||
labels: {
|
||||
formatter: (val: number): string => {
|
||||
return val + ' min';
|
||||
}
|
||||
}
|
||||
}],
|
||||
fill: {
|
||||
opacity: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
|
||||
this.isLoading = true;
|
||||
this.service.fetchDashboardStationCharts(
|
||||
this.bikePoint.id,
|
||||
actualStartDate,
|
||||
actualEndDate,
|
||||
chartType
|
||||
).then(source => {
|
||||
this.isLoading = false;
|
||||
setTimeout(() => {
|
||||
this.chart.updateSeries([{
|
||||
data: source.map(value => value.number)
|
||||
}, {
|
||||
data: source.map(value => Math.round(value.avgDuration / 60))
|
||||
}]);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-12 mb-md-3 mb-sm-3 mb-3">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Top-3 rental destination</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
This table shows the top-3 destinations of rentals from this station by number of drives.
|
||||
The Station can be sent to the map with the checkbox.
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<table [dataSource]="stationToSource" class="mat-elevation-z0 w-100" mat-table>
|
||||
<ng-container matColumnDef="select">
|
||||
<th *matHeaderCellDef mat-header-cell></th>
|
||||
<td *matCellDef="let row" class="p-3" mat-cell>
|
||||
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
|
||||
(click)="$event.stopPropagation()"
|
||||
[checked]="selectionModel.isSelected(row)"
|
||||
[disabled]="isCheckBoxDisable(row)"
|
||||
matTooltip="toggle to view marker on map"
|
||||
matTooltipPosition="above">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="endStationName">
|
||||
<th *matHeaderCellDef mat-header-cell>Destination</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
<a [routerLink]="['/dashboard/', element.stationId]">{{element.stationName}}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="number">
|
||||
<th *matHeaderCellDef mat-header-cell>Count</th>
|
||||
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="avgDuration">
|
||||
<th *matHeaderCellDef mat-header-cell>Average duration</th>
|
||||
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="marker">
|
||||
<th *matHeaderCellDef mat-header-cell>Icon</th>
|
||||
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumnsTo" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumnsTo;" mat-row></tr>
|
||||
</table>
|
||||
<div *ngIf="isLoadingToSource" class="col d-flex align-items-center justify-content-center">
|
||||
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Top-3 rental origin</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
This table shows the top-3 origins of rentals to this station by number of drives.
|
||||
The Station can be sent to the map with the checkbox.
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<table [dataSource]="stationFromSource" class="mat-elevation-z0 w-100" mat-table>
|
||||
<ng-container matColumnDef="select">
|
||||
<th *matHeaderCellDef mat-header-cell></th>
|
||||
<td *matCellDef="let row" class="p-3" mat-cell>
|
||||
<mat-checkbox (change)="$event ? selectRow($event, row) : null"
|
||||
(click)="$event.stopPropagation()"
|
||||
[checked]="selectionModel.isSelected(row)"
|
||||
[disabled]="isCheckBoxDisable(row)"
|
||||
matTooltip="toggle to view marker on map"
|
||||
matTooltipPosition="above">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="startStationName">
|
||||
<th *matHeaderCellDef mat-header-cell>Origin</th>
|
||||
<td *matCellDef="let element" mat-cell>
|
||||
<a [routerLink]="['/dashboard/', element.stationId]"> {{element.stationName}}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="number">
|
||||
<th *matHeaderCellDef mat-header-cell>Count</th>
|
||||
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="avgDuration">
|
||||
<th *matHeaderCellDef mat-header-cell>Average duration</th>
|
||||
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="marker">
|
||||
<th *matHeaderCellDef mat-header-cell>Icon</th>
|
||||
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumnsFrom" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumnsFrom;" mat-row></tr>
|
||||
</table>
|
||||
<div *ngIf="isLoadingFromSource" class="col d-flex align-items-center justify-content-center">
|
||||
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
img {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mat-checkbox-layout label {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mat-cell, .mat-header-cell {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TableComponent } from './table.component';
|
||||
|
||||
describe('TableComponent', () => {
|
||||
let component: TableComponent;
|
||||
let fixture: ComponentFixture<TableComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ TableComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,179 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from '@angular/material/table';
|
||||
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
|
||||
import {SelectionModel} from '@angular/cdk/collections';
|
||||
import {MatCheckboxChange} from '@angular/material/checkbox';
|
||||
import stht from 'seconds-to-human-time';
|
||||
import {MapService} from '../../service/map.service';
|
||||
import {DashboardService} from '../../service/dashboard.service';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-table',
|
||||
templateUrl: './table.component.html',
|
||||
styleUrls: ['./table.component.scss']
|
||||
})
|
||||
export class TableComponent implements OnInit {
|
||||
displayedColumnsTo: string[] = ['select', 'endStationName', 'number', 'avgDuration', 'marker'];
|
||||
displayedColumnsFrom: string[] = ['select', 'startStationName', 'number', 'avgDuration', 'marker'];
|
||||
|
||||
stationToSource = new MatTableDataSource<IDashboardCommonBikePoint>();
|
||||
iterableToSource: any[];
|
||||
stationFromSource = new MatTableDataSource<IDashboardCommonBikePoint>();
|
||||
iterableFromSource: any[];
|
||||
selectionModel = new SelectionModel<IDashboardCommonBikePoint>(true, []);
|
||||
colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
|
||||
bikePoint: IDashboardCommonBikePoint;
|
||||
maxStartDate: Date;
|
||||
maxEndDate: Date;
|
||||
isLoadingToSource = true;
|
||||
isLoadingFromSource = true;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private map: MapService,
|
||||
private service: DashboardService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.subscribe(params => {
|
||||
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
|
||||
this.service.fetchDashboardInit(params.id).then(data => {
|
||||
this.bikePoint = data;
|
||||
this.maxStartDate = new Date(data.maxStartDate);
|
||||
this.maxEndDate = new Date(data.maxEndDate);
|
||||
this.initTable();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTable(): void {
|
||||
this.selectionModel.clear();
|
||||
this.map.removeOverlayOnMiniMap();
|
||||
const initDate = this.maxEndDate.toISOString().substring(0, 10);
|
||||
this.loadData(initDate, initDate);
|
||||
}
|
||||
|
||||
onSubmit(actualStartDate: string, actualEndDate: string): void {
|
||||
this.resetTableSourcesToDisplaySpinner();
|
||||
this.selectionModel.clear();
|
||||
this.map.removeOverlayOnMiniMap();
|
||||
this.loadData(actualStartDate, actualEndDate);
|
||||
}
|
||||
|
||||
resetTableSourcesToDisplaySpinner(): void {
|
||||
this.isLoadingToSource = true;
|
||||
this.isLoadingFromSource = true;
|
||||
this.stationToSource = null;
|
||||
this.stationFromSource = null;
|
||||
this.iterableToSource = [];
|
||||
this.iterableFromSource = [];
|
||||
}
|
||||
|
||||
async loadData(actualStartDate: string, actualEndDate: string): Promise<void> {
|
||||
this.isLoadingToSource = true;
|
||||
this.isLoadingFromSource = true;
|
||||
|
||||
const [stationTo, stationFrom] = await Promise.all([
|
||||
this.service.fetchDashboardStationTo(this.bikePoint.id, actualStartDate, actualEndDate),
|
||||
this.service.fetchDashboardStationFrom(this.bikePoint.id, actualStartDate, actualEndDate)
|
||||
]);
|
||||
|
||||
this.isLoadingToSource = false;
|
||||
this.isLoadingFromSource = false;
|
||||
|
||||
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
|
||||
this.stationToSource = this.setBikePointColorToSource(stationTo);
|
||||
this.iterableToSource = stationTo;
|
||||
this.iterableToSource.forEach(bikePoint => bikePoint.polyLineColor = '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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<mat-card style="height: 30rem">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="d-flex align-items-center">
|
||||
<div class="header-image d-inline-flex" mat-card-avatar></div>
|
||||
{{bikePoint?.commonName}}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="p-4 d-flex flex-column justify-content-center align-content-between">
|
||||
<p>Select a range to analyze data</p>
|
||||
<form [formGroup]="form" (submit)="onSubmit()">
|
||||
<mat-form-field appearance="fill" class="w-100">
|
||||
<mat-label>Enter a range</mat-label>
|
||||
<mat-date-range-input [max]="maxEndDate" [min]="maxStartDate" [rangePicker]="picker"
|
||||
formGroupName="dateRange">
|
||||
<input formControlName="start" matStartDate placeholder="Start date">
|
||||
<input formControlName="end" matEndDate placeholder="End date">
|
||||
</mat-date-range-input>
|
||||
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
|
||||
<mat-date-range-picker #picker></mat-date-range-picker>
|
||||
</mat-form-field>
|
||||
<button (click)="onSubmit()" color="primary" type="submit" (keyup.enter)="onSubmit()" class="w-100" mat-raised-button>
|
||||
reload
|
||||
<mat-icon>cached</mat-icon>
|
||||
</button>
|
||||
</form>
|
||||
<br/>
|
||||
<div class="ml-n4" id="chart">
|
||||
<apx-chart
|
||||
[chart]="chartOptions.chart"
|
||||
[colors]="chartOptions.colors"
|
||||
[dataLabels]="chartOptions.dataLabels"
|
||||
[fill]="chartOptions.fill"
|
||||
[legend]="chartOptions.legend"
|
||||
[plotOptions]="chartOptions.plotOptions"
|
||||
[series]="chartOptions.series"
|
||||
[stroke]="chartOptions.stroke"
|
||||
[subtitle]="chartOptions.subtitle"
|
||||
[title]="chartOptions.title"
|
||||
[tooltip]="chartOptions.tooltip"
|
||||
[xaxis]="chartOptions.xaxis"
|
||||
[yaxis]="chartOptions.yaxis"
|
||||
></apx-chart>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@ -0,0 +1,4 @@
|
||||
.header-image {
|
||||
background-image: url('../../../assets/bike-point-blue.png');
|
||||
background-size: cover;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserInputComponent } from './user-input.component';
|
||||
|
||||
describe('UserInputComponent', () => {
|
||||
let component: UserInputComponent;
|
||||
let fixture: ComponentFixture<UserInputComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ UserInputComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,223 @@
|
||||
import {Component, EventEmitter, Injectable, OnInit, Output} from '@angular/core';
|
||||
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
|
||||
import {FormBuilder, FormControl, FormGroup} from '@angular/forms';
|
||||
import {IMapBikePoint} from '../../service/domain/map-bike-point';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {DashboardService} from '../../service/dashboard.service';
|
||||
import {
|
||||
ApexAxisChartSeries,
|
||||
ApexChart,
|
||||
ApexDataLabels,
|
||||
ApexFill,
|
||||
ApexLegend,
|
||||
ApexNoData,
|
||||
ApexPlotOptions,
|
||||
ApexStroke,
|
||||
ApexTitleSubtitle,
|
||||
ApexTooltip,
|
||||
ApexXAxis,
|
||||
ApexYAxis
|
||||
} from 'ng-apexcharts';
|
||||
import {DateAdapter, MAT_DATE_FORMATS, NativeDateAdapter} from '@angular/material/core';
|
||||
import {formatDate} from '@angular/common';
|
||||
|
||||
export type ChartOptions = {
|
||||
title: ApexTitleSubtitle;
|
||||
subtitle: ApexTitleSubtitle;
|
||||
series: ApexAxisChartSeries;
|
||||
chart: ApexChart;
|
||||
colors: string[];
|
||||
dataLabels: ApexDataLabels;
|
||||
plotOptions: ApexPlotOptions;
|
||||
yaxis: ApexYAxis | ApexYAxis[];
|
||||
xaxis: ApexXAxis;
|
||||
fill: ApexFill;
|
||||
tooltip: ApexTooltip;
|
||||
stroke: ApexStroke;
|
||||
legend: ApexLegend;
|
||||
noData: ApexNoData;
|
||||
};
|
||||
|
||||
export const PICK_FORMATS = {
|
||||
parse: {dateInput: {month: 'short', year: 'numeric', day: 'numeric'}},
|
||||
display: {
|
||||
dateInput: 'input',
|
||||
monthYearLabel: {year: 'numeric', month: 'numeric'},
|
||||
dateA11yLabel: {year: 'numeric', month: 'numeric', day: 'numeric'},
|
||||
monthYearA11yLabel: {year: 'numeric', month: 'long'}
|
||||
}
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
class PickDateAdapter extends NativeDateAdapter {
|
||||
format(date: Date, displayFormat: Object): string {
|
||||
if (displayFormat === 'input') {
|
||||
return formatDate(date, 'dd.MM.yyyy', this.locale);
|
||||
} else {
|
||||
return date.toDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface StartEndDate {
|
||||
actualStartDate: Date;
|
||||
actualEndDate: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-input',
|
||||
templateUrl: './user-input.component.html',
|
||||
styleUrls: ['./user-input.component.scss'],
|
||||
providers: [
|
||||
{provide: DateAdapter, useClass: PickDateAdapter},
|
||||
{provide: MAT_DATE_FORMATS, useValue: PICK_FORMATS}
|
||||
]
|
||||
})
|
||||
export class UserInputComponent implements OnInit {
|
||||
|
||||
@Output() startEndDate: EventEmitter<StartEndDate> = new EventEmitter<StartEndDate>();
|
||||
|
||||
chartOptions: Partial<ChartOptions>;
|
||||
|
||||
station: IDashboardCommonBikePoint;
|
||||
maxStartDate: Date;
|
||||
maxEndDate: Date;
|
||||
form: FormGroup;
|
||||
|
||||
bikePoint: IMapBikePoint;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private service: DashboardService,
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.chartOptions = {
|
||||
series: [],
|
||||
chart: {
|
||||
type: 'bar'
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
dateRange: new FormGroup({
|
||||
start: new FormControl(),
|
||||
end: new FormControl()
|
||||
})
|
||||
});
|
||||
this.route.params.subscribe(params => {
|
||||
this.service.fetchDashboardInit(params.id).then(data => {
|
||||
this.station = data;
|
||||
this.maxStartDate = new Date(data.maxStartDate);
|
||||
this.maxEndDate = new Date(data.maxEndDate);
|
||||
this.initInput().catch(error => console.log(error));
|
||||
});
|
||||
this.service.fetchBikePointForStatus(params.id).then(data => {
|
||||
this.bikePoint = data;
|
||||
const NbBlockedDocks = data.status.NbDocks - data.status.NbBikes - data.status.NbEmptyDocks;
|
||||
this.chartOptions = {
|
||||
subtitle: {
|
||||
text: 'This chart visualizes the availability of the bikes',
|
||||
offsetX: 20,
|
||||
offsetY: 15,
|
||||
style: {
|
||||
fontSize: '15px'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Bikes',
|
||||
data: [data.status.NbBikes]
|
||||
},
|
||||
{
|
||||
name: 'Empty docks',
|
||||
data: [data.status.NbEmptyDocks]
|
||||
},
|
||||
{
|
||||
name: 'Blocked docks',
|
||||
data: [NbBlockedDocks]
|
||||
}
|
||||
],
|
||||
colors: ['#51ca49', '#8f8e8e', '#f00'],
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 180,
|
||||
stacked: true,
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
dataLabels: {
|
||||
position: 'center'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '20px',
|
||||
colors: ['#fff']
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
show: false
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
title: {
|
||||
text: undefined
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
min: 0,
|
||||
max: data.status.NbDocks
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
horizontalAlign: 'right',
|
||||
fontSize: '14px'
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async initInput(): Promise<void> {
|
||||
const initDate = this.maxEndDate.toISOString().substring(0, 10);
|
||||
this.form.get('dateRange').get('start').setValue(initDate);
|
||||
this.form.get('dateRange').get('end').setValue(initDate);
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<any> {
|
||||
this.startEndDate.emit({
|
||||
actualStartDate: this.form.get('dateRange').value.start,
|
||||
actualEndDate: this.form.get('dateRange').value.end
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<div class="footer d-flex justify-content-center align-items-center">
|
||||
<div class="copyright">
|
||||
<span>© Tim Herbst & Marcel Schwarz</span>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
.footer {
|
||||
height: 2vh;
|
||||
background: #2f2f2f;
|
||||
|
||||
.copyright {
|
||||
color: white;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FooterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<mat-slide-toggle [(ngModel)]="isFlagActive" (ngModelChange)="onChange($event)">auto refresh</mat-slide-toggle>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
mat-slide-toggle {
|
||||
margin-right: 1em;
|
||||
font-size: 15px;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutoRefreshComponent } from './auto-refresh.component';
|
||||
|
||||
describe('AutoRefreshComponent', () => {
|
||||
let component: AutoRefreshComponent;
|
||||
let fixture: ComponentFixture<AutoRefreshComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AutoRefreshComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AutoRefreshComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {MapService} from '../../service/map.service';
|
||||
import * as internal from 'events';
|
||||
|
||||
@Component({
|
||||
selector: 'app-auto-refresh',
|
||||
templateUrl: './auto-refresh.component.html',
|
||||
styleUrls: ['./auto-refresh.component.scss']
|
||||
})
|
||||
export class AutoRefreshComponent implements OnInit, OnDestroy {
|
||||
isFlagActive: boolean;
|
||||
interval: internal;
|
||||
|
||||
constructor(private map: MapService) {
|
||||
const storageFlag = JSON.parse(sessionStorage.getItem('auto-refresh'));
|
||||
if (storageFlag) {
|
||||
this.isFlagActive = storageFlag;
|
||||
} else {
|
||||
this.isFlagActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.interval = setInterval(() => {
|
||||
if (this.isFlagActive) {
|
||||
this.map.autoRefresh().catch(error => console.log(error));
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onChange(flag: boolean): void {
|
||||
sessionStorage.setItem('auto-refresh', JSON.stringify(flag));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<div class="map-container" fxLayout="row">
|
||||
<div class="map-frame" fxFill>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
#map {
|
||||
height: 93vh;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MapComponent } from './map.component';
|
||||
|
||||
describe('MapComponent', () => {
|
||||
let component: MapComponent;
|
||||
let fixture: ComponentFixture<MapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MapComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
26
projects/project-3/frontend/src/app/map/map.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {MapService} from '../service/map.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-map',
|
||||
templateUrl: './map.component.html',
|
||||
styleUrls: ['./map.component.scss']
|
||||
})
|
||||
export class MapComponent implements OnInit {
|
||||
constructor(private service: MapService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initMapView();
|
||||
}
|
||||
|
||||
async initMapView(): Promise<any> {
|
||||
this.service.initMap(51.509865, -0.118092, 14);
|
||||
await this.service.drawStationMarkers();
|
||||
this.service.drawHeatmap();
|
||||
this.service.drawAccidents();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<mat-card class="mat-elevation-z0 p-0">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{station?.commonName}}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="d-flex flex-column align-items-center justify-content-center">
|
||||
<div id="chart" class="w-100 ml-n4">
|
||||
<apx-chart
|
||||
class="w-100"
|
||||
[chart]="chartOptions.chart"
|
||||
[colors]="chartOptions.colors"
|
||||
[dataLabels]="chartOptions.dataLabels"
|
||||
[fill]="chartOptions.fill"
|
||||
[legend]="chartOptions.legend"
|
||||
[plotOptions]="chartOptions.plotOptions"
|
||||
[series]="chartOptions.series"
|
||||
[stroke]="chartOptions.stroke"
|
||||
[title]="chartOptions.title"
|
||||
[tooltip]="chartOptions.tooltip"
|
||||
[xaxis]="chartOptions.xaxis"
|
||||
[yaxis]="chartOptions.yaxis"
|
||||
></apx-chart>
|
||||
</div>
|
||||
<button (click)="route()" mat-raised-button id="route-button">Open in dashboard</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@ -0,0 +1,14 @@
|
||||
#route-button {
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
#route-button {
|
||||
padding: 0 10px;
|
||||
background-color: #017bfe;
|
||||
color: white;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PopUpComponent } from './pop-up.component';
|
||||
|
||||
describe('PopUpComponent', () => {
|
||||
let component: PopUpComponent;
|
||||
let fixture: ComponentFixture<PopUpComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PopUpComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PopUpComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,118 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {IMapBikePoint} from '../../service/domain/map-bike-point';
|
||||
import {Router} from '@angular/router';
|
||||
import {
|
||||
ApexAxisChartSeries,
|
||||
ApexChart,
|
||||
ApexDataLabels,
|
||||
ApexFill,
|
||||
ApexLegend,
|
||||
ApexPlotOptions,
|
||||
ApexStroke,
|
||||
ApexTitleSubtitle,
|
||||
ApexTooltip,
|
||||
ApexXAxis,
|
||||
ApexYAxis
|
||||
} from 'ng-apexcharts';
|
||||
|
||||
export type ChartOptions = {
|
||||
series: ApexAxisChartSeries;
|
||||
chart: ApexChart;
|
||||
colors: string[];
|
||||
dataLabels: ApexDataLabels;
|
||||
plotOptions: ApexPlotOptions;
|
||||
xaxis: ApexXAxis;
|
||||
yaxis: ApexYAxis;
|
||||
stroke: ApexStroke;
|
||||
title: ApexTitleSubtitle;
|
||||
tooltip: ApexTooltip;
|
||||
fill: ApexFill;
|
||||
legend: ApexLegend;
|
||||
};
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-pop-up',
|
||||
templateUrl: './pop-up.component.html',
|
||||
styleUrls: ['./pop-up.component.scss']
|
||||
})
|
||||
export class PopUpComponent implements OnInit {
|
||||
station: IMapBikePoint;
|
||||
public chartOptions: Partial<ChartOptions>;
|
||||
|
||||
constructor(private router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const NbBlockedDocks = this.station.status.NbDocks - this.station.status.NbBikes - this.station.status.NbEmptyDocks;
|
||||
this.chartOptions = {
|
||||
series: [
|
||||
{
|
||||
name: 'Bikes',
|
||||
data: [this.station.status.NbBikes]
|
||||
},
|
||||
{
|
||||
name: 'Empty docks',
|
||||
data: [this.station.status.NbEmptyDocks]
|
||||
},
|
||||
{
|
||||
name: 'Blocked docks',
|
||||
data: [NbBlockedDocks]
|
||||
}
|
||||
],
|
||||
colors: ['#51ca49', '#8f8e8e', '#f00'],
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 125,
|
||||
stacked: true,
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
show: false
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
title: {
|
||||
text: undefined
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
min: 0,
|
||||
max: this.station.status.NbDocks
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
horizontalAlign: 'right'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public route(): void {
|
||||
this.router.navigate(['/dashboard', this.station.id]);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
describe('DashboardService', () => {
|
||||
let service: DashboardService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(DashboardService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {environment} from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DashboardService {
|
||||
|
||||
constructor(private client: HttpClient) {
|
||||
}
|
||||
|
||||
public fetchDashboardInit(id: string): Promise<any> {
|
||||
return this.client.get(environment.apiUrl + `latest/dashboard/${id}/`).toPromise();
|
||||
}
|
||||
|
||||
public fetchBikePointForStatus(id: string): Promise<any> {
|
||||
return this.client.get(environment.apiUrl + `latest/bikepoints/${id}/`).toPromise();
|
||||
}
|
||||
|
||||
public fetchDashboardStationTo(id: string, startDate: string, endDate: string): Promise<any> {
|
||||
return this.client.get(
|
||||
environment.apiUrl + `latest/dashboard/${id}/to?start_date=${startDate}&end_date=${endDate}`
|
||||
).toPromise();
|
||||
}
|
||||
|
||||
public fetchDashboardStationFrom(id: string, startDate: string, endDate: string): Promise<any> {
|
||||
return this.client.get(
|
||||
environment.apiUrl + `latest/dashboard/${id}/from?start_date=${startDate}&end_date=${endDate}`
|
||||
).toPromise();
|
||||
}
|
||||
|
||||
public fetchDashboardStationCharts(id: string, startDate: string, endDate: string, type: string): Promise<any> {
|
||||
return this.client.get(
|
||||
environment.apiUrl + `latest/dashboard/${id}/${type}?start_date=${startDate}&end_date=${endDate}`
|
||||
).toPromise();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
export interface IDashboardCommonBikePoint {
|
||||
id?: string;
|
||||
color?: string;
|
||||
commonName?: string;
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
maxEndDate?: string;
|
||||
maxStartDate?: string;
|
||||
}
|
||||
|
||||
export class DashboardCommonBikePoint implements IDashboardCommonBikePoint {
|
||||
constructor(
|
||||
public id?: string,
|
||||
public color?: string,
|
||||
public commonName?: string,
|
||||
public lat?: number,
|
||||
public lon?: number,
|
||||
public maxEndDate?: string,
|
||||
public maxStartDate?: string
|
||||
) {
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
export interface IMapBikePoint {
|
||||
id?: string;
|
||||
commonName?: string;
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
status?: BikePointStatus;
|
||||
}
|
||||
|
||||
export class MapBikePoint implements IMapBikePoint {
|
||||
constructor(
|
||||
public id?: string,
|
||||
public commonName?: string,
|
||||
public lat?: number,
|
||||
public lon?: number,
|
||||
public status?: BikePointStatus
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
export class BikePointStatus {
|
||||
NbBikes?: number;
|
||||
NbEmptyDocks?: number;
|
||||
NbDocks?: number;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MapService } from './map.service';
|
||||
|
||||
describe('MapService', () => {
|
||||
let service: MapService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(MapService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
256
projects/project-3/frontend/src/app/service/map.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PopUpService } from './pop-up.service';
|
||||
|
||||
describe('PopUpService', () => {
|
||||
let service: PopUpService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(PopUpService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFactoryResolver, Injectable, Injector} from '@angular/core';
|
||||
import {IMapBikePoint} from './domain/map-bike-point';
|
||||
import {PopUpComponent} from '../map/pop-up/pop-up.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PopUpService {
|
||||
|
||||
constructor(
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private injector: Injector
|
||||
) {
|
||||
}
|
||||
|
||||
makeAvailabilityPopUp(station: IMapBikePoint): any {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(PopUpComponent);
|
||||
const component = factory.create(this.injector);
|
||||
|
||||
component.instance.station = station;
|
||||
component.changeDetectorRef.detectChanges();
|
||||
|
||||
return component.location.nativeElement;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<div class="d-flex">
|
||||
<a color="primary"
|
||||
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203"
|
||||
mat-flat-button
|
||||
target="_blank">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
Wiki
|
||||
</a>
|
||||
<a color="primary" mat-flat-button routerLink="/">
|
||||
<mat-icon>map</mat-icon>
|
||||
back to map
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
a:hover {
|
||||
background: #086ed2;
|
||||
text-decoration: none;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardInteractionComponent } from './dashboard-interaction.component';
|
||||
|
||||
describe('DashboardInteractionComponent', () => {
|
||||
let component: DashboardInteractionComponent;
|
||||
let fixture: ComponentFixture<DashboardInteractionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardInteractionComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardInteractionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-interaction',
|
||||
templateUrl: './dashboard-interaction.component.html',
|
||||
styleUrls: ['./dashboard-interaction.component.scss']
|
||||
})
|
||||
export class DashboardInteractionComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<div class="d-flex">
|
||||
<app-auto-refresh></app-auto-refresh>
|
||||
<a class="button-wiki" color="primary"
|
||||
href="https://gitlab.com/marcel.schwarz/geovisualisierung/-/wikis/Projektarbeit%203" mat-flat-button
|
||||
target="_blank">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
Wiki
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
.button-wiki:hover {
|
||||
background: #086ed2;
|
||||
text-decoration: none;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MapInteractionComponent } from './map-interaction.component';
|
||||
|
||||
describe('InteractionComponent', () => {
|
||||
let component: MapInteractionComponent;
|
||||
let fixture: ComponentFixture<MapInteractionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MapInteractionComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MapInteractionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-map-interaction',
|
||||
templateUrl: './map-interaction.component.html',
|
||||
styleUrls: ['./map-interaction.component.scss']
|
||||
})
|
||||
export class MapInteractionComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<mat-toolbar class="mat-toolbar" color="primary">
|
||||
<span routerLink="/" id="logo">
|
||||
<img alt="" src="../../assets/Logo.png" height="40" class="mx-2 mb-1">
|
||||
Bike Stations in London
|
||||
</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<div *ngIf="!hasRoute('dashboard')">
|
||||
<app-map-interaction></app-map-interaction>
|
||||
</div>
|
||||
<div *ngIf="hasRoute('dashboard')">
|
||||
<app-dashboard-interaction></app-dashboard-interaction>
|
||||
</div>
|
||||
</mat-toolbar>
|
@ -0,0 +1,7 @@
|
||||
.toolbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.mat-toolbar {
|
||||
height: 5vh;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToolbarComponent } from './toolbar.component';
|
||||
|
||||
describe('ToolbarComponent', () => {
|
||||
let component: ToolbarComponent;
|
||||
let fixture: ComponentFixture<ToolbarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ToolbarComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ToolbarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toolbar',
|
||||
templateUrl: './toolbar.component.html',
|
||||
styleUrls: ['./toolbar.component.scss']
|
||||
})
|
||||
export class ToolbarComponent implements OnInit {
|
||||
|
||||
constructor(private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
hasRoute(route: string): boolean {
|
||||
return this.router.url.includes(route);
|
||||
}
|
||||
|
||||
}
|
0
projects/project-3/frontend/src/assets/.gitkeep
Normal file
BIN
projects/project-3/frontend/src/assets/Logo.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-black.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-blue.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-gray.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-green.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-orange.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-purple.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
projects/project-3/frontend/src/assets/bike-point-red.png
Normal file
After Width: | Height: | Size: 76 KiB |
@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiUrl: 'https://it-schwarz.net/api/'
|
||||
};
|
17
projects/project-3/frontend/src/environments/environment.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://it-schwarz.net/api/'
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
BIN
projects/project-3/frontend/src/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
15
projects/project-3/frontend/src/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Frontend</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
projects/project-3/frontend/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
63
projects/project-3/frontend/src/polyfills.ts
Normal 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
|
||||
*/
|
77
projects/project-3/frontend/src/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
25
projects/project-3/frontend/src/test.ts
Normal 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);
|
246
projects/project-3/frontend/src/theme.scss
Normal 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';
|
||||
}
|
||||
}
|
15
projects/project-3/frontend/tsconfig.app.json
Normal 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"
|
||||
]
|
||||
}
|
20
projects/project-3/frontend/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
18
projects/project-3/frontend/tsconfig.spec.json
Normal 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"
|
||||
]
|
||||
}
|
152
projects/project-3/frontend/tslint.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|