Finalize project
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
@@ -0,0 +1,13 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# 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
|
||||
@@ -0,0 +1,17 @@
|
||||
# Stage 1
|
||||
|
||||
FROM node:alpine AS build-image
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm ci && npm run build -- --configuration=production
|
||||
|
||||
# Stage 2
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build-image /app/dist/app-fe /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,14 @@
|
||||
# AIC Angular Frontend
|
||||
|
||||
## Tips
|
||||
* use Jetbrains Webstorm IDE
|
||||
* don't forget your browser's console (F12)
|
||||
|
||||
## Prerequisites
|
||||
* install [nodejs](https://nodejs.org/en/)
|
||||
* install [angular-cli globally](https://cli.angular.io/)
|
||||
|
||||
## Setup
|
||||
$ npm install
|
||||
|
||||
$ ng serve
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"app-fe": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/app-fe",
|
||||
"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": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"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": "app-fe:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "app-fe:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "app-fe: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"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"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": "app-fe:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "app-fe:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "app-fe",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('frontend-dev App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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-htmlfile-reporter'),
|
||||
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'), reports: ['html', 'lcovonly'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
|
||||
htmlReporter: {
|
||||
outputFile: 'unit-tests.html',
|
||||
|
||||
// Optional
|
||||
pageTitle: 'Unit Tests',
|
||||
subPageTitle: 'A sample project description',
|
||||
groupSuites: true,
|
||||
useCompactStyle: true,
|
||||
useLegacyStyle: true
|
||||
},
|
||||
|
||||
reporters: ['progress', 'html'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome', 'ChromeHeadless'],
|
||||
customLaunchers: {
|
||||
ChromeHeadless: {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
'--headless',
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9222'
|
||||
]
|
||||
}
|
||||
},
|
||||
singleRun: true
|
||||
});
|
||||
};
|
||||
+40396
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "app-fe",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"postinstall": "ngcc"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@agm/core": "^3.0.0-beta.0",
|
||||
"@angular-devkit/schematics": "~9.1.7",
|
||||
"@angular/animations": "~9.1.9",
|
||||
"@angular/cdk": "9.2.4",
|
||||
"@angular/common": "~9.1.9",
|
||||
"@angular/compiler": "~9.1.9",
|
||||
"@angular/core": "~9.1.9",
|
||||
"@angular/forms": "~9.1.9",
|
||||
"@angular/localize": "~9.1.9",
|
||||
"@angular/material": "9.2.4",
|
||||
"@angular/platform-browser": "~9.1.9",
|
||||
"@angular/platform-browser-dynamic": "~9.1.9",
|
||||
"@angular/router": "~9.1.9",
|
||||
"@types/uuid": "8.3.0",
|
||||
"bootstrap": "~4.3.1",
|
||||
"jquery": "3.5.1",
|
||||
"ngx-logger": "4.1.9",
|
||||
"popper.js": "1.16.0",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.901.7",
|
||||
"@angular/cli": "~9.1.7",
|
||||
"@angular/compiler-cli": "~9.1.9",
|
||||
"@angular/language-service": "~9.1.9",
|
||||
"@compodoc/compodoc": "~1.1.11",
|
||||
"@types/googlemaps": "^3.39.13",
|
||||
"@types/jasmine": "~3.4.6",
|
||||
"@types/jasminewd2": "~2.0.8",
|
||||
"@types/node": "^12.12.42",
|
||||
"codelyzer": "^5.2.2",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.4.1",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.1.1",
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.5.4",
|
||||
"protractor": "~5.4.4",
|
||||
"ts-node": "~8.5.4",
|
||||
"tslint": "~5.20.1",
|
||||
"typescript": "~3.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const {SpecReporter} = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.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: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
const routes: Routes = [];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -0,0 +1,49 @@
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {LandingComponent} from './component/landing/landing.component';
|
||||
import {RestService} from './services/rest.service';
|
||||
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
|
||||
import {InterceptorService} from './services/interceptor.service';
|
||||
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
|
||||
import {environment} from '../environments/environment';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import {AgmCoreModule, GoogleMapsAPIWrapper} from '@agm/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LandingComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
LoggerModule.forRoot({level: environment.log_level, serverLogLevel: NgxLoggerLevel.ERROR}),
|
||||
HttpClientModule,
|
||||
MatFormFieldModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatSlideToggleModule,
|
||||
AgmCoreModule.forRoot({
|
||||
apiKey: 'AIzaSyCXF-INAlIkG9uPBofGLUCyHthaLxN-3PY',
|
||||
})
|
||||
],
|
||||
// enables injecting
|
||||
providers: [
|
||||
GoogleMapsAPIWrapper,
|
||||
RestService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true
|
||||
},
|
||||
|
||||
],
|
||||
bootstrap: [LandingComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#map{
|
||||
height : 100vh; width: 100%; padding:0px; margin: 0px;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<agm-map disableDefaultUI="true" id="map" #map [zoom]="zoom" [latitude]="center.lat" [longitude]="center.lng">
|
||||
|
||||
<agm-marker (markerClick)="openInfoWindow(m.value.carEntity.vin)" [openInfoWindow]="true"
|
||||
*ngFor="let m of car_markers | keyvalue" [iconUrl]="getIconUrlCar(m.value.carEvent.near_crash_event)"
|
||||
[latitude]="m.value.carEvent.gps_location.latitude" [longitude]="m.value.carEvent.gps_location.longitude" [animation]="(m.value.carEvent.near_crash_event)?'BOUNCE':''">
|
||||
<agm-info-window style="opacity: 0.5!important;" (infoWindowClose)="closeInfoWindow(m.value.carEntity.vin)"
|
||||
[isOpen]="isInfoWindowOpen(m.value.carEntity.vin)" [latitude]="m.value.carEvent.gps_location.latitude"
|
||||
[longitude]="m.value.carEvent.gps_location.longitude">
|
||||
<p>VIN: {{m.value.carEntity.vin}}</p>
|
||||
<p>OEM: {{m.value.carEntity.oem}}</p>
|
||||
<p>Model Type: {{m.value.carEntity.modelType}}</p>
|
||||
<p>Velocity: {{m.value.carEvent.velocity}}</p>
|
||||
<p>Timestamp: {{m.value.carEvent.timestamp}}</p>
|
||||
<p>NCE: {{m.value.carEvent.near_crash_event}}</p>
|
||||
</agm-info-window>
|
||||
</agm-marker>
|
||||
|
||||
<agm-marker *ngFor="let m of traffic_light_markers | keyvalue" [iconUrl]="trafficLightToImage(m.value.trafficLightEvent.color)"
|
||||
[latitude]="m.value.trafficLightEntity.location[1]" [longitude]="m.value.trafficLightEntity.location[0]">
|
||||
<agm-info-window>
|
||||
<p>Id: {{m.value.trafficLightEntity.id}}</p>
|
||||
<p>Switching Time: {{m.value.trafficLightEntity.switchingTime}}</p>
|
||||
<p>Range: {{m.value.trafficLightEntity.range}}</p>
|
||||
<p>Color: {{m.value.trafficLightEvent.color}}</p>
|
||||
<p>Last Switch: {{m.value.trafficLightEvent.last_switch}}</p>
|
||||
</agm-info-window>
|
||||
</agm-marker>
|
||||
|
||||
</agm-map>
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import {AgmMap} from '@agm/core';
|
||||
import {RestService} from '../../services/rest.service';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {interval, Subscription} from 'rxjs';
|
||||
import {startWith, switchMap} from 'rxjs/operators';
|
||||
import {
|
||||
Car,
|
||||
CarEntity,
|
||||
CarEvent,
|
||||
GeoCoordinates, TrafficLight,
|
||||
TrafficLightEntity, TrafficLightEvent
|
||||
} from '../../interfaces/interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing',
|
||||
templateUrl: './landing.component.html',
|
||||
styleUrls: ['./landing.component.css']
|
||||
})
|
||||
export class LandingComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private restService: RestService,
|
||||
private logger: NGXLogger
|
||||
) {
|
||||
}
|
||||
|
||||
@ViewChild('map') map: AgmMap;
|
||||
|
||||
|
||||
zoom = 14;
|
||||
center = {lat: 47.90620, lng: 16.20785};
|
||||
|
||||
traffic_light_markers = new Map();
|
||||
car_markers = new Map();
|
||||
infoWindows = new Map();
|
||||
|
||||
timeInterval: Subscription;
|
||||
|
||||
TL_RED_IMAGE = 'assets/pictures/traffic_light_red.png';
|
||||
TL_GREEN_IMAGE = 'assets/pictures/traffic_light_green.png';
|
||||
CAR_IMAGE = 'assets/pictures/car.png';
|
||||
CAR_IMAGE_NCE = 'assets/pictures/car_orange.png';
|
||||
NCE_SOUND = 'assets/sound/crash.mp3';
|
||||
|
||||
ngOnInit() {
|
||||
this.getTrafficLights();
|
||||
this.getCars();
|
||||
}
|
||||
|
||||
|
||||
getCarEvents(vin) {
|
||||
this.timeInterval = interval(1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.restService.getCarEvents(vin))
|
||||
).subscribe((data: any) => {
|
||||
if (data.body === '') {
|
||||
return;
|
||||
}
|
||||
const carEvent = data.body as CarEvent;
|
||||
const car = this.car_markers.get(vin) as Car;
|
||||
car.carEvent = carEvent;
|
||||
this.car_markers.set(car['vin'], car);
|
||||
|
||||
},
|
||||
err => this.logger.error(err),
|
||||
() => {
|
||||
this.logger.debug('loaded car events');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getTrafficLightEvents(tlid) {
|
||||
|
||||
this.timeInterval = interval(1000)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
switchMap(() => this.restService.getTrafficLightEvents(tlid))
|
||||
).subscribe(
|
||||
(data: any) => {
|
||||
const trafficLightEvent = data.body as TrafficLightEvent;
|
||||
const trafficLight = this.traffic_light_markers.get(tlid) as TrafficLight;
|
||||
trafficLight.trafficLightEvent = trafficLightEvent;
|
||||
this.traffic_light_markers.set(tlid, trafficLight);
|
||||
},
|
||||
err => this.logger.error(err),
|
||||
() => {
|
||||
this.logger.debug('loaded traffic light events');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getTrafficLights() {
|
||||
this.restService.getTrafficLights().subscribe(
|
||||
(data: TrafficLightEntity[]) => {
|
||||
for (const trafficLightEntity of data) {
|
||||
|
||||
const trafficLight: TrafficLight = {
|
||||
trafficLightEntity: trafficLightEntity,
|
||||
trafficLightEvent: this.getEmptyTrafficLightEvent()
|
||||
};
|
||||
this.traffic_light_markers.set(trafficLightEntity['id'], trafficLight);
|
||||
}
|
||||
},
|
||||
err => this.logger.error(err),
|
||||
() => {
|
||||
this.logger.debug('loaded traffic lights');
|
||||
for (const value of this.traffic_light_markers.values()) {
|
||||
this.getTrafficLightEvents(value.trafficLightEntity['id']);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getCars() {
|
||||
this.restService.getCars().subscribe(
|
||||
(data: CarEntity[]) => {
|
||||
for (const carEntity of data) {
|
||||
const car: Car = {
|
||||
carEntity: carEntity,
|
||||
carEvent: this.getEmptyCarEvent()
|
||||
};
|
||||
this.car_markers.set(carEntity['vin'], car);
|
||||
}
|
||||
},
|
||||
err => this.logger.error(err),
|
||||
() => {
|
||||
this.logger.debug('loaded cars');
|
||||
for (const value of this.car_markers.values()) {
|
||||
this.getCarEvents(value.carEntity['vin']);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
trafficLightToImage(trafficLight) {
|
||||
return (trafficLight === 'RED') ? this.TL_RED_IMAGE : this.TL_GREEN_IMAGE;
|
||||
}
|
||||
|
||||
getIconUrlCar(near_crash_event) {
|
||||
if (near_crash_event === true) {
|
||||
this.playCrashSound();
|
||||
return this.CAR_IMAGE_NCE;
|
||||
} else {
|
||||
return this.CAR_IMAGE;
|
||||
}
|
||||
}
|
||||
|
||||
openInfoWindow(id) {
|
||||
this.infoWindows.set(id, true);
|
||||
}
|
||||
|
||||
closeInfoWindow(id) {
|
||||
this.infoWindows.delete(id);
|
||||
}
|
||||
|
||||
isInfoWindowOpen(id) {
|
||||
return this.infoWindows.has(id);
|
||||
}
|
||||
|
||||
playCrashSound() {
|
||||
const audio = new Audio();
|
||||
audio.src = this.NCE_SOUND;
|
||||
audio.load();
|
||||
audio.play();
|
||||
}
|
||||
|
||||
private getEmptyGeoCoordinates(): GeoCoordinates {
|
||||
return {
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
};
|
||||
}
|
||||
|
||||
private getEmptyCarEvent(): CarEvent {
|
||||
return {
|
||||
near_crash_event: false,
|
||||
gps_location: this.getEmptyGeoCoordinates(),
|
||||
timestamp: '',
|
||||
velocity: 0
|
||||
};
|
||||
}
|
||||
|
||||
private getEmptyTrafficLightEvent(): TrafficLightEvent {
|
||||
return {
|
||||
last_switch: 0,
|
||||
color: '',
|
||||
tlid: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
export interface WSEvents {
|
||||
message: Observable<Event>;
|
||||
error: Observable<Event>;
|
||||
close: Observable<Event>;
|
||||
}
|
||||
|
||||
export interface GeoCoordinates {
|
||||
latitude: Number;
|
||||
longitude: Number;
|
||||
}
|
||||
|
||||
export interface CarEntity {
|
||||
oem: String;
|
||||
modelType: String;
|
||||
vin: String;
|
||||
}
|
||||
|
||||
export interface CarEvent {
|
||||
near_crash_event: Boolean;
|
||||
gps_location: GeoCoordinates;
|
||||
timestamp: String;
|
||||
velocity: Number;
|
||||
}
|
||||
|
||||
export interface Car {
|
||||
carEntity: CarEntity;
|
||||
carEvent: CarEvent;
|
||||
}
|
||||
|
||||
export interface TrafficLightEntity {
|
||||
id: Number;
|
||||
location: Number[];
|
||||
range: Number;
|
||||
switchingTime: Number;
|
||||
color: String;
|
||||
}
|
||||
|
||||
export interface TrafficLightEvent {
|
||||
last_switch: Number;
|
||||
color: String;
|
||||
tlid: Number;
|
||||
}
|
||||
|
||||
export interface TrafficLight {
|
||||
trafficLightEntity: TrafficLightEntity;
|
||||
trafficLightEvent: TrafficLightEvent;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {tap} from 'rxjs/operators';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* The InterceptorService intercepts EVERY http event.
|
||||
*/
|
||||
export class InterceptorService implements HttpInterceptor {
|
||||
|
||||
constructor(private logger: NGXLogger) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts every HTTP request and for example adds a token
|
||||
* @param req http request
|
||||
* @param next http response handler
|
||||
*/
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// TODO get the real token
|
||||
const token = 'A_TOKEN';
|
||||
|
||||
// the original request is immutable, so we need to clone it
|
||||
req = req.clone({
|
||||
setHeaders: {
|
||||
// FIXME e.g. if Bearer is used
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
//this.logger.debug('Interceptor works');
|
||||
|
||||
// pipe the response observable
|
||||
return next.handle(req).pipe(
|
||||
// perform a side effect for every emission on the source Observable
|
||||
// return an Observable that is identical to the source
|
||||
tap(event => {
|
||||
// check if it is the response message
|
||||
if (event instanceof HttpResponse) {
|
||||
// TODO so something if necessary
|
||||
}
|
||||
}, error => {
|
||||
// TODO handle error here
|
||||
this.logger.error('HTTP Response interception error', error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {environment} from '../../environments/environment';
|
||||
|
||||
@Injectable()
|
||||
export class RestService {
|
||||
private currentLocation = environment.location + '/api/v1/resources/';
|
||||
|
||||
constructor(
|
||||
private logger: NGXLogger,
|
||||
private http: HttpClient
|
||||
) {
|
||||
}
|
||||
|
||||
getTrafficLightEvents(tlid) {
|
||||
return this.http.get(this.currentLocation + 'traffic_light_events?id=' + tlid, {observe: 'response'});
|
||||
}
|
||||
|
||||
getCarEvents(vin) {
|
||||
return this.http.get(this.currentLocation + 'car_events?vin=' + vin, {observe: 'response'});
|
||||
}
|
||||
|
||||
getTrafficLights() {
|
||||
return this.http.get(this.currentLocation + 'traffic_lights');
|
||||
}
|
||||
|
||||
getCars() {
|
||||
return this.http.get(this.currentLocation + 'cars');
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 316 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 275 B |
Binary file not shown.
|
After Width: | Height: | Size: 303 B |
Binary file not shown.
|
After Width: | Height: | Size: 269 B |
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
import {NgxLoggerLevel} from 'ngx-logger';
|
||||
|
||||
export const environment = {
|
||||
production: true,
|
||||
location: '/xway',
|
||||
log_level: NgxLoggerLevel.WARN,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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`.
|
||||
|
||||
import {NgxLoggerLevel} from 'ngx-logger';
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
location: 'http://xway:5004',
|
||||
log_level: NgxLoggerLevel.DEBUG,
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AppFe</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCXF-INAlIkG9uPBofGLUCyHthaLxN-3PY"></script>
|
||||
</head>
|
||||
<body>
|
||||
<app-landing></app-landing>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!--<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>-->
|
||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>-->
|
||||
<!--<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>-->
|
||||
@@ -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));
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
(window as any).global = window;
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/weak-map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
||||
// import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
/**
|
||||
* Required to support Web Animations `@angular/platform-browser/animations`.
|
||||
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
|
||||
**/
|
||||
// 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
|
||||
*/
|
||||
|
||||
// (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__BLACK_LISTED_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
|
||||
*/
|
||||
import '@angular/localize/init';
|
||||
@@ -0,0 +1,14 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||
@import '~bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gm-style-iw-a {
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,20 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@include mat-core();
|
||||
|
||||
$my-app-primary: mat-palette($mat-grey);
|
||||
$my-app-accent: mat-palette($mat-amber, 500, 900, A100);
|
||||
$my-app-warn: mat-palette($mat-red, 700, 900);
|
||||
|
||||
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
|
||||
|
||||
@include angular-material-theme($my-app-theme);
|
||||
|
||||
.alternate-theme {
|
||||
$alternate-primary: mat-palette($mat-light-blue);
|
||||
$alternate-accent: mat-palette($mat-yellow, 400);
|
||||
|
||||
$alternate-theme: mat-light-theme($alternate-primary, $alternate-accent);
|
||||
|
||||
@include angular-material-theme($alternate-theme);
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2015",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"rulesDirectory": [
|
||||
"node_modules/codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-return-shorthand": true,
|
||||
"callable-types": true,
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warn"
|
||||
},
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"interface-over-type-literal": true,
|
||||
"label-position": true,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-super": true,
|
||||
"no-empty": false,
|
||||
"no-empty-interface": true,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-misused-new": true,
|
||||
"no-non-null-assertion": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-string-throw": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"prefer-const": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"unified-signatures": true,
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
],
|
||||
"no-output-on-prefix": true,
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
"use-host-property-decorator": true,
|
||||
"no-input-rename": true,
|
||||
"no-output-rename": true,
|
||||
"use-life-cycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"component-class-suffix": true,
|
||||
"directive-class-suffix": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
ENV PORT 5002
|
||||
EXPOSE 5002
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "entity_ident_server.py" ]
|
||||
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
from bson import json_util
|
||||
from flask import Flask, request
|
||||
from flask_pymongo import PyMongo
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["MONGO_URI"] = "mongodb://mongo:27017/entities"
|
||||
mongo = PyMongo(app)
|
||||
|
||||
CAR1_SV = int(os.environ.get('DSE2021_CAR1_SV', 130))
|
||||
CAR2_SV = int(os.environ.get('DSE2021_CAR2_SV', 130))
|
||||
CAR3_SV = int(os.environ.get('DSE2021_CAR3_SV', 130))
|
||||
|
||||
CAR1_SD = int(os.environ.get('DSE2021_CAR1_SD', 300))
|
||||
CAR2_SD = int(os.environ.get('DSE2021_CAR2_SD', 500))
|
||||
CAR3_SD = int(os.environ.get('DSE2021_CAR3_SD', 400))
|
||||
|
||||
CAR1_ST = int(os.environ.get('DSE2021_CAR1_ST', 10))
|
||||
CAR2_ST = int(os.environ.get('DSE2021_CAR2_ST', 15))
|
||||
CAR3_ST = int(os.environ.get('DSE2021_CAR3_ST', 25))
|
||||
|
||||
TL1_R = int(os.environ.get('DSE2021_TL1_R', 2000))
|
||||
TL2_R = int(os.environ.get('DSE2021_TL2_R', 800))
|
||||
TL3_R = int(os.environ.get('DSE2021_TL3_R', 1000))
|
||||
|
||||
mongo.db.trafficLights.update_one({"id": "1"}, {"$set": {"range": TL1_R}})
|
||||
mongo.db.trafficLights.update_one({"id": "2"}, {"$set": {"range": TL2_R}})
|
||||
mongo.db.trafficLights.update_one({"id": "3"}, {"$set": {"range": TL3_R}})
|
||||
|
||||
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingVelocity": CAR1_SV}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingVelocity": CAR2_SV}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingVelocity": CAR3_SV}})
|
||||
|
||||
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingDistance": CAR1_SD}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingDistance": CAR2_SD}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingDistance": CAR3_SD}})
|
||||
|
||||
mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingTime": CAR1_ST}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingTime": CAR2_ST}})
|
||||
mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingTime": CAR3_ST}})
|
||||
|
||||
|
||||
@app.route('/api/v1/resources/cars', methods=['GET'])
|
||||
def get_cars():
|
||||
query_parameters = request.args
|
||||
vin = query_parameters.get('vin')
|
||||
|
||||
if vin is not None:
|
||||
cars = [car for car in mongo.db.cars.find({'vin': vin})]
|
||||
else:
|
||||
cars = [car for car in mongo.db.cars.find({})]
|
||||
|
||||
return json_util.dumps({'cursor': cars})
|
||||
|
||||
|
||||
@app.route('/api/v1/resources/traffic_lights', methods=['GET'])
|
||||
def get_traffic_lights():
|
||||
query_parameters = request.args
|
||||
tl_id = query_parameters.get('id')
|
||||
|
||||
if tl_id is not None:
|
||||
traffic_lights = [traffic_light for traffic_light in
|
||||
mongo.db.trafficLights.find({'id': tl_id})]
|
||||
else:
|
||||
traffic_lights = [traffic_light for traffic_light in mongo.db.trafficLights.find({})]
|
||||
|
||||
return json_util.dumps({'cursor': traffic_lights})
|
||||
|
||||
|
||||
@app.route('/api/v1/resources/traffic_lights_geo', methods=['GET'])
|
||||
def get_traffic_lights_geo():
|
||||
query_parameters = request.args
|
||||
|
||||
lat = float(query_parameters.get('lat'))
|
||||
lon = float(query_parameters.get('lon'))
|
||||
|
||||
traffic_lights = [
|
||||
traffic_light for traffic_light in
|
||||
mongo.db.trafficLights.aggregate([
|
||||
{"$geoNear": {
|
||||
"near": {
|
||||
"type": "Point",
|
||||
"coordinates": [lon, lat]
|
||||
},
|
||||
"spherical": True,
|
||||
"distanceField": "calculatedRange"
|
||||
}},
|
||||
{"$redact": {
|
||||
"$cond": {
|
||||
"if": {
|
||||
"$lte": ["$calculatedRange", "$range"]
|
||||
},
|
||||
"then": "$$KEEP",
|
||||
"else": "$$PRUNE"
|
||||
}
|
||||
}}
|
||||
]
|
||||
)
|
||||
if lat < traffic_light['location'][1]
|
||||
]
|
||||
|
||||
return json_util.dumps({'cursor': traffic_lights[:1]})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run('0.0.0.0', 5002)
|
||||
@@ -0,0 +1,3 @@
|
||||
flask
|
||||
Flask-PyMongo
|
||||
jsonify
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM mongo:4.4
|
||||
|
||||
COPY ./cars.json .
|
||||
COPY ./traffic_lights.json .
|
||||
|
||||
ADD import.sh /docker-entrypoint-initdb.d
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"oem": "BENTLEY",
|
||||
"modelType": "Continental",
|
||||
"vin": "SCBFR7ZA5CC072256"
|
||||
},
|
||||
{
|
||||
"oem": "SATURN",
|
||||
"modelType": "Vue",
|
||||
"vin": "5GZCZ43D13S812715"
|
||||
},
|
||||
{
|
||||
"oem": "SATURN",
|
||||
"modelType": "Vue2",
|
||||
"vin": "5GZCZ43D13S812716"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
mongoimport --db entities --collection cars --type json --file cars.json --jsonArray
|
||||
mongoimport --db entities --collection trafficLights --type json --file traffic_lights.json --jsonArray
|
||||
|
||||
mongo --eval 'db.trafficLights.ensureIndex({location:"2dsphere"})' entities
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"location": [16.20719, 47.89584],
|
||||
"switchingTime": 26,
|
||||
"color": "RED"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"location": [16.20814, 47.90937],
|
||||
"switchingTime": 16,
|
||||
"color": "GREEN"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"location": [16.20917, 47.92703],
|
||||
"switchingTime": 20,
|
||||
"color": "RED"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM redis
|
||||
ENV PORT 6379
|
||||
ENV HOSTNAME redis
|
||||
EXPOSE 6379
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
RUN cd deps; ls; pip install dse-shared-libs-1.tar.gz;
|
||||
ENV PORT 5001
|
||||
EXPOSE 5001
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "event_store_server.py" ]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity
|
||||
from dse_shared_libs.daf import DAF
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from redis import StrictRedis
|
||||
|
||||
sys.modules['daf'] = daf
|
||||
sys.modules['traffic_light_state'] = traffic_light_state
|
||||
sys.modules['traffic_light_color'] = traffic_light_color
|
||||
sys.modules['target_velocity'] = target_velocity
|
||||
|
||||
|
||||
class EventLogger:
|
||||
def __init__(self, redis: StrictRedis, log_to_redis: bool, verbose: bool):
|
||||
"""
|
||||
:param redis:
|
||||
:param log_to_redis:
|
||||
:param verbose:
|
||||
"""
|
||||
self.redis = redis
|
||||
self.log_to_redis = log_to_redis
|
||||
self.verbose = verbose
|
||||
|
||||
def setup_msq_queue(self):
|
||||
"""
|
||||
Sets up the message queue which calls self.log on msg receive.
|
||||
"""
|
||||
mb = MBWrapper(exchange_name='logger', callback=self.log)
|
||||
mb.setup_receiver()
|
||||
|
||||
def get_keys(self):
|
||||
"""
|
||||
:returns: the available redis keys.
|
||||
"""
|
||||
return json.dumps([key.decode() for key in self.redis.keys()])
|
||||
|
||||
def get_list_of(self, key: str):
|
||||
"""
|
||||
Returns a list of all elements inside a key. Most recent elements are stored first.
|
||||
|
||||
:param key: DAF:<VIN> or TV:<VIN> for either DAF events or TV events
|
||||
:returns: list of elements
|
||||
"""
|
||||
key = self.redis.lrange(key, 0, -1)
|
||||
return key.decode() if key else json.dumps("")
|
||||
|
||||
def get_index_of(self, key: str, index: int):
|
||||
"""
|
||||
Returns element matching index in key. Most recent elements are stored first.
|
||||
|
||||
:param key: DAF:<VIN> or TV:<VIN> for either DAF events or TV events
|
||||
:param index: 0 for most recent
|
||||
"""
|
||||
key = self.redis.lindex(key, index)
|
||||
return key.decode() if key else json.dumps("")
|
||||
|
||||
def log(self, msg: bytes):
|
||||
"""
|
||||
Logging callback. Logs the msg depending on its type to the respective redis key.
|
||||
Types can be DAF, TrafficLightState or TargetVelocity. If type is unknown, message is still logged as key
|
||||
UNKNOWN.
|
||||
|
||||
:param bytes msg: pickled msg binary to log
|
||||
"""
|
||||
key, to_log = self._unpack_message_to_log(msg)
|
||||
|
||||
if self.log_to_redis:
|
||||
self.redis.lpush(key, "{}".format(to_log))
|
||||
if self.verbose:
|
||||
print(to_log)
|
||||
|
||||
def _unpack_message_to_log(self, msg: bytes):
|
||||
try:
|
||||
msg = pickle.loads(msg)
|
||||
except Exception as e:
|
||||
print("Could not unpickle [{}]: {}", msg, e)
|
||||
self.redis: StrictRedis
|
||||
self.redis.append('ERRORS', str(e))
|
||||
|
||||
key: str
|
||||
if type(msg) == DAF:
|
||||
msg: DAF
|
||||
key = 'DAF:{}'.format(msg.vehicle_identification_number)
|
||||
elif type(msg) == TrafficLightState:
|
||||
msg: TrafficLightState
|
||||
key = 'TL:{}'.format(msg.tlid)
|
||||
elif type(msg) == TargetVelocity:
|
||||
msg: TargetVelocity
|
||||
key = 'TV:{}'.format(msg.vin)
|
||||
else:
|
||||
key = 'UNKNOWN'
|
||||
|
||||
try:
|
||||
to_log = json.dumps(msg.to_dict())
|
||||
except AttributeError:
|
||||
try:
|
||||
to_log = json.dumps(msg)
|
||||
except (json.decoder.JSONDecodeError, TypeError):
|
||||
to_log = str(msg)
|
||||
|
||||
return key, to_log
|
||||
@@ -0,0 +1,68 @@
|
||||
import threading
|
||||
|
||||
from flask import Flask
|
||||
from flask_redis import Redis
|
||||
from redis import StrictRedis
|
||||
|
||||
from event_logger import EventLogger
|
||||
|
||||
# TOGGLE if every received message shall be redirected to redis
|
||||
LOG_TO_REDIS = True
|
||||
# TOGGLE if every received message shall be printed to console
|
||||
VERBOSE = True
|
||||
|
||||
# make sure redis (container) is running and accessible
|
||||
|
||||
# see https://flask-and-redis.readthedocs.io/en/latest/
|
||||
# see https://github.com/andymccurdy/redis-py
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["REDIS_HOST"] = "redis"
|
||||
app.config["REDIS_PORT"] = 6379
|
||||
redis: StrictRedis = Redis(app)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def get_keys():
|
||||
keys = redis.keys()
|
||||
keys.sort()
|
||||
return 'Existing keys:<br><br>{}'.format(
|
||||
['<a href="/api/keys/{0}/">{0}</a>'.format(key.decode()) for key in keys]) + \
|
||||
'DAF: DatenaufzeichnungAutonomesFahren, TL: TrafficLight, TV: TargetVelocity' \
|
||||
'<br><br>' \
|
||||
'<br><br>Access Api via <br><br>' \
|
||||
'GET /api/keys<br>' \
|
||||
'GET /api/keys/<key><br>'
|
||||
|
||||
|
||||
@app.route("/api/keys")
|
||||
def api_get_keys():
|
||||
"""
|
||||
:return: list of available redis keys
|
||||
"""
|
||||
return el.get_keys()
|
||||
|
||||
|
||||
@app.route("/api/keys/<key>/")
|
||||
def api_get_of_key(key):
|
||||
"""
|
||||
:param key: key to get data of
|
||||
:return: list of data to the respective key
|
||||
"""
|
||||
return el.get_list_of(key)
|
||||
|
||||
|
||||
@app.route("/api/keys/<key>/<index>/")
|
||||
def api_get_index_of_key(key, index):
|
||||
"""
|
||||
:param key: key to get data element of
|
||||
:param index: index of keys data list
|
||||
:return: element matching index of key
|
||||
"""
|
||||
return el.get_index_of(key, index)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
el = EventLogger(redis, LOG_TO_REDIS, VERBOSE)
|
||||
threading.Thread(target=el.setup_msq_queue).start()
|
||||
threading.Thread(target=app.run, args=('0.0.0.0', 5001)).start()
|
||||
@@ -0,0 +1,2 @@
|
||||
flask
|
||||
Flask-And-Redis
|
||||
@@ -0,0 +1,68 @@
|
||||
import datetime
|
||||
import json
|
||||
import pickle
|
||||
import unittest
|
||||
|
||||
import geopy
|
||||
from dse_shared_libs.daf import DAF
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from redis import StrictRedis
|
||||
|
||||
from event_logger import EventLogger
|
||||
|
||||
|
||||
class TestEventLogger(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.el = EventLogger(StrictRedis(), False, False)
|
||||
self.timestamp = datetime.datetime.utcnow()
|
||||
|
||||
def test_unpack_daf(self):
|
||||
daf = DAF(vehicle_identification_number='my_vin',
|
||||
gps_location=geopy.Point(0, 0, 0),
|
||||
velocity=130.0,
|
||||
near_crash_event=False,
|
||||
timestamp=self.timestamp,
|
||||
)
|
||||
|
||||
key, message = self.el._unpack_message_to_log(pickle.dumps(daf))
|
||||
|
||||
self.assertEqual(key, 'DAF:my_vin')
|
||||
self.assertEqual(message, json.dumps(daf.to_dict()))
|
||||
|
||||
def test_unpack_target_velocity(self):
|
||||
tv = TargetVelocity(vin='my_other_vin',
|
||||
target_velocity=120.0,
|
||||
timestamp=self.timestamp)
|
||||
|
||||
key, message = self.el._unpack_message_to_log(pickle.dumps(tv))
|
||||
|
||||
self.assertEqual(key, 'TV:my_other_vin')
|
||||
self.assertEqual(message, json.dumps(tv.to_dict()))
|
||||
|
||||
def test_unpack_traffic_light_state(self):
|
||||
tls = TrafficLightState(tlid='my_traffic_light',
|
||||
color=TrafficLightColor.GREEN,
|
||||
last_switch=self.timestamp)
|
||||
|
||||
key, message = self.el._unpack_message_to_log(pickle.dumps(tls))
|
||||
|
||||
self.assertEqual(key, 'TL:my_traffic_light')
|
||||
self.assertEqual(message, json.dumps(tls.to_dict()))
|
||||
|
||||
def test_unpack_unknown_dict(self):
|
||||
unknown = dict(foo='foo', bar='bar')
|
||||
|
||||
key, message = self.el._unpack_message_to_log(pickle.dumps(unknown))
|
||||
|
||||
self.assertEqual(key, 'UNKNOWN')
|
||||
self.assertEqual(message, json.dumps(unknown))
|
||||
|
||||
def test_unpack_unknown_object(self):
|
||||
obj = datetime.datetime.utcnow()
|
||||
|
||||
key, message = self.el._unpack_message_to_log(pickle.dumps(obj))
|
||||
|
||||
self.assertEqual(key, 'UNKNOWN')
|
||||
self.assertEqual(message, str(obj))
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
RUN cd deps; ls; pip install dse-shared-libs-1.tar.gz;
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "i_feed.py" ]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,99 @@
|
||||
import pickle
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from circuitbreaker import circuit
|
||||
|
||||
# Default switching time in seconds
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from pika.exceptions import AMQPConnectionError
|
||||
|
||||
SWITCHING_TIME = 15
|
||||
# Scale speed of switching by factor x
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
|
||||
|
||||
class TrafficLight:
|
||||
# id of traffic light
|
||||
tlid: str
|
||||
# time between switches in seconds
|
||||
switching_time: int
|
||||
# timestamp of last switch
|
||||
last_switch: datetime
|
||||
# currently lit color
|
||||
current_color: TrafficLightColor
|
||||
# marks if traffic light started
|
||||
running: bool = False
|
||||
# stores starting color
|
||||
_starting_color: TrafficLightColor
|
||||
# stores runner thread
|
||||
_t: threading.Thread
|
||||
|
||||
# outgoing traffic light state MBWrapper
|
||||
_tl_mb: MBWrapper
|
||||
|
||||
def __init__(self,
|
||||
tlid: str,
|
||||
switching_time: int = SWITCHING_TIME,
|
||||
starting_color: TrafficLightColor = TrafficLightColor.RED):
|
||||
"""
|
||||
Init a new Traffic Light client. This will already initialize its message queue.
|
||||
Start it with self.start().
|
||||
|
||||
:param tlid: ID of the traffic light
|
||||
:param switching_time: time in seconds to switch between states (TrafficLightColor.RED and .GREEN)
|
||||
:param starting_color: TrafficLightColor to start with
|
||||
"""
|
||||
self.tlid = tlid
|
||||
self.switching_time = switching_time
|
||||
self._starting_color = starting_color
|
||||
|
||||
self._tl_mb = MBWrapper(exchange_name='traffic_light_state_{}'.format(self.tlid))
|
||||
self._tl_mb.setup_sender()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the traffic light by spawning a new thread..
|
||||
"""
|
||||
self.running = True
|
||||
self._t = threading.Thread(target=self._switcher)
|
||||
self._t.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
The running thread will stop its execution at the next evaluation of while self.running.
|
||||
"""
|
||||
self.running = False
|
||||
|
||||
@circuit(failure_threshold=10, expected_exception=AMQPConnectionError)
|
||||
def send_status_update(self):
|
||||
"""
|
||||
Sends a status update containing an updated TrafficLightState object to the orchestrator.
|
||||
This update contains the tlid, current_color and timestamp of the last switch.
|
||||
"""
|
||||
print(self.tlid, self.current_color, self.last_switch)
|
||||
self._tl_mb.send(pickle.dumps(
|
||||
TrafficLightState(tlid=self.tlid, color=self.current_color, last_switch=self.last_switch)))
|
||||
|
||||
def _switcher(self):
|
||||
"""
|
||||
Toggles the traffic lights state every self.switching_time / SCALING seconds.
|
||||
When it switches, it notifies the orchestrator with self.send_status_update()
|
||||
"""
|
||||
num_colors = len(TrafficLightColor)
|
||||
# set current color to the one before starting color, because switch immediately happens in loop
|
||||
# therefore it sleeps on the end of the loop and first status is immediately sent to msg broker
|
||||
self.current_color = TrafficLightColor((self._starting_color.value - 1) % num_colors)
|
||||
while self.running:
|
||||
self.current_color = TrafficLightColor((self.current_color.value + 1) % num_colors)
|
||||
self.last_switch = datetime.utcnow()
|
||||
self.send_status_update()
|
||||
time.sleep(self.switching_time / SCALING)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
...
|
||||
@@ -0,0 +1,277 @@
|
||||
import pickle
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
import geopy
|
||||
import geopy.distance
|
||||
from circuitbreaker import circuit
|
||||
from dse_shared_libs.daf import DAF
|
||||
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
|
||||
# Lat, Long
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from pika.exceptions import AMQPConnectionError
|
||||
|
||||
STARTING_POINT = geopy.Point(47.89053, 16.20703)
|
||||
# Driving direction in degrees: 0=N, 90=E, 180=S, 270=W
|
||||
BEARING = 2
|
||||
# Scale speed of vehicles by factor x
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
# in km/h
|
||||
STARTING_VELOCITY = 130 * SCALING
|
||||
# Interval between status updates in seconds (is not scaled)
|
||||
UPDATE_INTERVAL = 1
|
||||
# Specify if NCE should happen
|
||||
NCE = int(os.environ.get('DSE2021_NCE', 1))
|
||||
# At x km the NCE shall happen
|
||||
NCE_KM = 2.4
|
||||
# Time in seconds to recover from NCE (will be scaled)
|
||||
TIME_TO_RECOVER = 15
|
||||
# Resets vehicle at km x
|
||||
RESET_KM = 4.3
|
||||
|
||||
|
||||
class Vehicle:
|
||||
# vehicle identification number
|
||||
vin: str
|
||||
# velocity in km/h
|
||||
velocity: float
|
||||
# stores the timestamp most recently used to calculate driving distance
|
||||
last_update: Union[datetime, None]
|
||||
# marks if vehicle is currently driving, a new one isn't, has to be started via self.start_driving()
|
||||
driving: bool = False
|
||||
# stores last velocity before NCE, is used to recover after NCE
|
||||
_last_velocity: float
|
||||
# stores the so-far driven kilometers
|
||||
_driven_kms: int
|
||||
# stores the starting point for resetting the vehicle
|
||||
_starting_point: geopy.Point
|
||||
# stores the gps_location, shall be accessed by gps_location, where the actual value is calculated and returned
|
||||
_gps_location: geopy.Point
|
||||
# stores if nce possible
|
||||
_nce_possible = True
|
||||
# stores if nce already happened, it only happens (up to) once
|
||||
_nce_happened = False
|
||||
# stores if we are still recovering from NCE
|
||||
_recovering = False
|
||||
|
||||
# continuous driving thread
|
||||
_t: threading.Thread
|
||||
# NCE recovery thread
|
||||
_rt: threading.Thread
|
||||
|
||||
# outgoing daf info MBWrapper
|
||||
_daf_mb: MBWrapper
|
||||
# incoming target velocity MBWrapper
|
||||
_velocity_mb: MBWrapper
|
||||
|
||||
def __init__(self,
|
||||
vin: str,
|
||||
starting_point: geopy.Point = STARTING_POINT,
|
||||
starting_velocity: float = STARTING_VELOCITY):
|
||||
"""
|
||||
Initialize a new vehicle client. Already initializes the corresponding sending and receiving message queue.
|
||||
A vehicle sends every UPDATE_INTERVAL seconds its current DAF to the orchestrator. It receives as response the
|
||||
new TargetVelocity to achieve a possible green wave. After a fixed amount of kilometers, the NCE event is
|
||||
happening. The car then stands still for TIME_TO_RECOVER seconds. Afterwards, it starts driving with the last
|
||||
known velocity. While it is recovering, it ignores the target velocity responses from the orchestrator.
|
||||
A vehicle drives a full route between start and end. After reaching the end, it restores the internal state and
|
||||
starts again from the beginning.
|
||||
|
||||
:param vin: Vehicle Identification Number
|
||||
:param starting_point: point on globe to start at
|
||||
:param starting_velocity: velocity to start driving with
|
||||
"""
|
||||
self.vin = vin
|
||||
self._starting_point = starting_point
|
||||
self._gps_location = starting_point
|
||||
self.velocity = starting_velocity
|
||||
|
||||
self._daf_mb = MBWrapper(exchange_name='vehicle_daf_{}'.format(vin))
|
||||
self._daf_mb.setup_sender()
|
||||
|
||||
self._velocity_mb = MBWrapper(exchange_name='vehicle_velocity_{}'.format(self.vin), callback=self.new_velocity)
|
||||
self._velocity_mb.setup_receiver()
|
||||
|
||||
@property
|
||||
def nce(self):
|
||||
"""
|
||||
On accessing this property, it is calculated if the NCE shall happen. NCE only happens up to once per route.
|
||||
If the NCE happens, the car stops driving (velocity = 0) for TIME_TO_RECOVER seconds. In this time,
|
||||
responses from the orchestrator are ignored. After recovery, the vehicle starts driving again with the last
|
||||
known velocity.
|
||||
|
||||
:return: True if NCE invoked, otherwise False
|
||||
"""
|
||||
d = geopy.distance.distance(kilometers=0.4)
|
||||
tl2_loc = geopy.Point(47.90937, 16.20814)
|
||||
# Calculate point 400m south of traffic light 2
|
||||
nce_point = d.destination(point=tl2_loc, bearing=182)
|
||||
if NCE == 1 and self._nce_possible and not self._nce_happened:
|
||||
if self._gps_location.latitude >= nce_point.latitude:
|
||||
self._nce_happened = True
|
||||
self._last_velocity = self.velocity
|
||||
self.velocity = 0
|
||||
|
||||
def _sleep_and_recover():
|
||||
recover_in = TIME_TO_RECOVER / SCALING
|
||||
self._recovering = True
|
||||
print('\nNCE !!! Recovering in {} (scaled) seconds.\n'.format(recover_in))
|
||||
time.sleep(recover_in)
|
||||
print('\nRecovered.\n')
|
||||
self._recovering = False
|
||||
self.velocity = self._last_velocity
|
||||
|
||||
self._rt = threading.Thread(target=_sleep_and_recover)
|
||||
self._rt.start()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@nce.setter
|
||||
def nce(self, val):
|
||||
self._nce_happened = val
|
||||
|
||||
@property
|
||||
def daf(self):
|
||||
"""
|
||||
Return the DAF object of the vehicle. The properties are always evaluated on calling. That guarantees accurate
|
||||
values.
|
||||
|
||||
:return: current "Datenaufzeichnung für automatisiertes Fahren" (DAF) object
|
||||
"""
|
||||
|
||||
# ATTENTION: ORDER MANDATORY (except for static vin)
|
||||
return DAF(vehicle_identification_number=self.vin,
|
||||
# first deduce nce - is calculated, sets velocity to 0 if NCE
|
||||
near_crash_event=self.nce,
|
||||
# then get current location based on last location and current speed
|
||||
gps_location=self.gps_location,
|
||||
# finally get last_update, is updated by gps_location deduction
|
||||
timestamp=self.last_update,
|
||||
# is set to 0 if nce
|
||||
velocity=self.velocity,
|
||||
)
|
||||
|
||||
@property
|
||||
def gps_location(self):
|
||||
"""
|
||||
An accurate current position of the vehicle at the moment the property is called is retunred.
|
||||
The gps location is derived from the last position this property was called at and the
|
||||
time the vehicle was driving since then. Therefore, it is not necessary to call this function exactly at a given
|
||||
time span, because it calculates the driven kms relative to the calling time.
|
||||
"""
|
||||
# Define starting point.
|
||||
start = self._gps_location
|
||||
|
||||
# Get old and updated timestamps
|
||||
old_timestamp = self.last_update
|
||||
updated_timestamp = datetime.utcnow()
|
||||
self.last_update = updated_timestamp
|
||||
|
||||
# get driving time between timestamps (in seconds)
|
||||
driving_time = (updated_timestamp - old_timestamp).total_seconds()
|
||||
|
||||
# reached distance in kilometers: convert km/h to km/s and multiply by driving time
|
||||
kilometers = self.velocity / 3600 * driving_time
|
||||
|
||||
# Define a general distance object, initialized with a distance of k km.
|
||||
d = geopy.distance.distance(kilometers=kilometers)
|
||||
|
||||
# Use the `destination` method with a bearing of BEARING degrees
|
||||
# in order to move from point `start` to kilometers km bearing.
|
||||
self._gps_location = d.destination(point=start, bearing=BEARING)
|
||||
|
||||
# update driven kilometers
|
||||
self._driven_kms += kilometers
|
||||
return self._gps_location
|
||||
|
||||
@property
|
||||
def driven_kms(self):
|
||||
"""
|
||||
:returns: a string representation of the driven kms
|
||||
"""
|
||||
return '{}km'.format(round(self._driven_kms, 2))
|
||||
|
||||
def start_driving(self):
|
||||
"""
|
||||
Starts a thread responsible for driving. This thread continuously updates internal values and
|
||||
informs the message broker about the current state (DAF) of the vehicle.
|
||||
"""
|
||||
print('{} starts driving ... SCALING: x{}\n\n'.format(self.vin, SCALING))
|
||||
self.last_update = datetime.utcnow()
|
||||
self._driven_kms = 0
|
||||
|
||||
self._t = threading.Thread(target=self.drive)
|
||||
self._t.start()
|
||||
|
||||
def drive(self):
|
||||
"""
|
||||
Drive until not self.driving == update status every UPDATE_INTERVAL seconds.
|
||||
"""
|
||||
self.driving = True
|
||||
while self.driving:
|
||||
self.check_reset()
|
||||
self.send_status_update()
|
||||
time.sleep(UPDATE_INTERVAL)
|
||||
|
||||
def stop_driving(self):
|
||||
"""
|
||||
Set self.driving to stop driving thread gracefully.
|
||||
"""
|
||||
self.driving = False
|
||||
self._gps_location = self._starting_point
|
||||
self._driven_kms = 0
|
||||
|
||||
def check_reset(self):
|
||||
"""
|
||||
Checks if end of route is reached and resets vehicle to starting conditions.
|
||||
It also resets on malicious coordinates.
|
||||
|
||||
Afterwards, the vehicle is still driving, but again from start.
|
||||
"""
|
||||
if self._driven_kms >= RESET_KM:
|
||||
self._reset()
|
||||
|
||||
if self._gps_location.latitude < self._starting_point.latitude or \
|
||||
self._gps_location.longitude < self._starting_point.longitude:
|
||||
self._reset()
|
||||
|
||||
@circuit(failure_threshold=10, expected_exception=AMQPConnectionError)
|
||||
def send_status_update(self):
|
||||
"""
|
||||
Sends the current DAF to the orchestrator. The orchestrator will then respond asynchronously on the response
|
||||
queue.
|
||||
"""
|
||||
print(self.driven_kms, '\t', self.daf)
|
||||
self._daf_mb.send(pickle.dumps(self.daf))
|
||||
|
||||
def new_velocity(self, response: bytes):
|
||||
"""
|
||||
Will be invoked if new target velocity message received from orchestrator.
|
||||
:param response: pickled TargetVelocity object
|
||||
"""
|
||||
response: TargetVelocity = pickle.loads(response)
|
||||
velocity = response.target_velocity
|
||||
print('Received new velocity {}'.format(velocity))
|
||||
|
||||
if not self._recovering:
|
||||
self.velocity = velocity
|
||||
else:
|
||||
print('We are still recovering ... ignoring new target velocity.')
|
||||
|
||||
def _reset(self):
|
||||
print('\n\nEnd of route reached ... resetting and restarting vehicle')
|
||||
self._gps_location = self._starting_point
|
||||
self._driven_kms = 0
|
||||
self.last_update = datetime.utcnow()
|
||||
self.nce = False
|
||||
self.velocity = STARTING_VELOCITY
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
...
|
||||
@@ -0,0 +1,55 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import geopy.distance
|
||||
|
||||
import requests
|
||||
from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity
|
||||
|
||||
from devices.traffic_light import TrafficLight
|
||||
from devices.vehicle import Vehicle
|
||||
|
||||
sys.modules['daf'] = daf
|
||||
sys.modules['traffic_light_state'] = traffic_light_state
|
||||
sys.modules['traffic_light_color'] = traffic_light_color
|
||||
sys.modules['target_velocity'] = target_velocity
|
||||
|
||||
"""
|
||||
USED AS ENTRYPOINT FOR DOCKERFILE
|
||||
Start all vehicles / traffic lights here
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
|
||||
available = False
|
||||
while not available:
|
||||
try:
|
||||
response = requests.get(ENTITY_IDENT_URL + 'cars')
|
||||
cars = response.json()
|
||||
|
||||
response = requests.get(ENTITY_IDENT_URL + 'traffic_lights')
|
||||
traffic_lights = response.json()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the entity_ident_server running and reachable?")
|
||||
continue
|
||||
available = True
|
||||
|
||||
for traffic_light in traffic_lights['cursor']:
|
||||
print(traffic_light)
|
||||
TrafficLight(tlid=traffic_light['id'],
|
||||
switching_time=traffic_light['switchingTime']).start()
|
||||
time.sleep(0.5)
|
||||
|
||||
for index, car in enumerate(cars['cursor']):
|
||||
print(car)
|
||||
# Get location of first TL
|
||||
tl_1_loc = geopy.Point(47.89584, 16.20719)
|
||||
# Calculate starting point by going south of TL_location the amount specified in cars.json
|
||||
d = geopy.distance.distance(kilometers=car['startingDistance'] / float(1000))
|
||||
starting_point = d.destination(point=tl_1_loc, bearing=182)
|
||||
starting_velocity = car['startingVelocity']
|
||||
print('StartingPoint is {} and startingVelocity is {}'.format(starting_point, starting_velocity))
|
||||
Vehicle(vin=car['vin'], starting_point=starting_point, starting_velocity=starting_velocity).start_driving()
|
||||
# Sleep for amount of seconds defined in second car before second car starts
|
||||
if len(cars['cursor']) > 1 and index < len(cars['cursor']) - 1:
|
||||
time.sleep(cars['cursor'][index + 1]['startingTime'])
|
||||
@@ -0,0 +1,3 @@
|
||||
circuitbreaker # fault tolerance
|
||||
geopy
|
||||
requests
|
||||
@@ -0,0 +1,113 @@
|
||||
import datetime
|
||||
import pickle
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import geopy
|
||||
from dse_shared_libs.daf import DAF
|
||||
from dse_shared_libs.mock.datetime import MyDate
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
|
||||
from devices.traffic_light import TrafficLight
|
||||
from devices.vehicle import Vehicle
|
||||
from dse_shared_libs.mock.rabbit_mq_mocks import MyBlockingConnection
|
||||
|
||||
|
||||
class TestVehicle(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
with patch('pika.BlockingConnection', MyBlockingConnection):
|
||||
self.vin = 'my_vin'
|
||||
self.starting_velocity = 130
|
||||
self.starting_point = geopy.Point(0, 0, 0)
|
||||
self.timestamp = datetime.datetime.utcnow()
|
||||
|
||||
self.v = Vehicle(
|
||||
vin=self.vin,
|
||||
starting_point=self.starting_point,
|
||||
starting_velocity=self.starting_velocity
|
||||
)
|
||||
|
||||
self.v.last_update = self.timestamp
|
||||
self.v._driven_kms = 0
|
||||
|
||||
def tearDown(self) -> None:
|
||||
time.sleep(0.1)
|
||||
|
||||
def test_nce_prop(self):
|
||||
with patch('time.sleep', return_value=None):
|
||||
# initially false
|
||||
self.assertEqual(self.v.nce, False)
|
||||
# get past nce km
|
||||
self.v._gps_location.latitude = 48
|
||||
# now nce should fire
|
||||
self.assertEqual(self.v.nce, True)
|
||||
# second call is false again
|
||||
self.assertEqual(self.v.nce, False)
|
||||
|
||||
def test_daf_prop(self):
|
||||
with patch('devices.vehicle.datetime', MyDate):
|
||||
MyDate.set_timestamp(self.timestamp)
|
||||
|
||||
# test if daf object created properly
|
||||
daf = DAF(vehicle_identification_number=self.vin,
|
||||
gps_location=self.starting_point,
|
||||
velocity=self.starting_velocity,
|
||||
timestamp=self.timestamp,
|
||||
near_crash_event=False)
|
||||
self.assertEqual(self.v.daf, daf)
|
||||
|
||||
def test_current_location_prop(self):
|
||||
with patch('devices.vehicle.datetime', MyDate):
|
||||
MyDate.set_timestamp(self.timestamp)
|
||||
|
||||
# initial position should be starting point
|
||||
self.assertEqual(self.v.gps_location, self.starting_point)
|
||||
|
||||
# lets say last time was about an hour ago
|
||||
self.v.last_update = self.timestamp - datetime.timedelta(hours=1)
|
||||
|
||||
# we drive "almost" directly north
|
||||
loc = self.v.gps_location
|
||||
self.assertEqual(round(loc.latitude, 2), 1.17)
|
||||
self.assertEqual(round(loc.longitude, 2), 0.04)
|
||||
|
||||
def test_driven_kms_prop(self):
|
||||
# initial driven kms are 0
|
||||
self.assertEqual(self.v.driven_kms, '0km')
|
||||
|
||||
# alter driven kms
|
||||
self.v._driven_kms = 44.24552
|
||||
# will be rounded to 2 digits
|
||||
self.assertEqual(self.v.driven_kms, '44.25km')
|
||||
|
||||
def test_new_velocity(self):
|
||||
# starting velocity is set to 130
|
||||
self.assertEqual(self.v.velocity, 130)
|
||||
|
||||
# we get a new velocity, lets say 55 ...
|
||||
tv = TargetVelocity(vin='my_vin', target_velocity=55, timestamp=self.timestamp)
|
||||
self.v.new_velocity(pickle.dumps(tv))
|
||||
# then the vehicle should comply with that
|
||||
self.assertEqual(self.v.velocity, 55)
|
||||
|
||||
|
||||
class TestTrafficLight(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
with patch('pika.BlockingConnection', MyBlockingConnection):
|
||||
self.tl = TrafficLight(tlid='my_tl',
|
||||
switching_time=1,
|
||||
starting_color=TrafficLightColor.RED)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.tl.stop()
|
||||
|
||||
def test_switching(self):
|
||||
self.assertFalse(hasattr(self.tl, 'current_color'))
|
||||
self.tl.start()
|
||||
self.assertEqual(self.tl.current_color, TrafficLightColor.RED)
|
||||
time.sleep(1.2)
|
||||
self.assertEqual(self.tl.current_color, TrafficLightColor.GREEN)
|
||||
time.sleep(1)
|
||||
self.assertEqual(self.tl.current_color, TrafficLightColor.RED)
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
RUN cd deps; ls; pip install dse-shared-libs-1.tar.gz;
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "orchestration_server.py" ]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from orchestrator import Orchestrator
|
||||
|
||||
if __name__ == '__main__':
|
||||
Orchestrator().setup_msg_queues()
|
||||
@@ -0,0 +1,178 @@
|
||||
import datetime
|
||||
import pickle
|
||||
import sys
|
||||
import os
|
||||
from typing import List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from math import floor
|
||||
|
||||
import requests
|
||||
from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity
|
||||
from dse_shared_libs.daf import DAF
|
||||
from dse_shared_libs.message_broker_wrapper import MBWrapper
|
||||
# necessary to unpickle daf object
|
||||
from dse_shared_libs.target_velocity import TargetVelocity
|
||||
from dse_shared_libs.traffic_light_state import TrafficLightState
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
from requests import Session
|
||||
|
||||
sys.modules['daf'] = daf
|
||||
sys.modules['traffic_light_state'] = traffic_light_state
|
||||
sys.modules['traffic_light_color'] = traffic_light_color
|
||||
sys.modules['target_velocity'] = target_velocity
|
||||
|
||||
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
|
||||
|
||||
SCALING = int(os.environ.get('DSE2021_SCALING', 1))
|
||||
|
||||
|
||||
class Orchestrator:
|
||||
# vehicle ids
|
||||
vins: List[str] = []
|
||||
# traffic lights {tlid: {color: color, switching_time: in_seconds}}
|
||||
tls: Dict[str, Dict] = {}
|
||||
|
||||
# cache all active incoming daf connections
|
||||
_daf_mbs: Dict[str, MBWrapper] = {}
|
||||
_velocity_mbs: Dict[str, MBWrapper] = {}
|
||||
|
||||
_session: Session
|
||||
|
||||
def __init__(self):
|
||||
# re-use http session, is faster then request.ing everytime
|
||||
self._session = requests.Session()
|
||||
|
||||
available = False
|
||||
while not available:
|
||||
try:
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'cars')
|
||||
cars = response.json()
|
||||
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'traffic_lights')
|
||||
traffic_lights = response.json()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the entity_ident_server running and reachable?")
|
||||
continue
|
||||
available = True
|
||||
|
||||
for car in cars['cursor']:
|
||||
self.vins.append(car['vin'])
|
||||
|
||||
for traffic_light in traffic_lights['cursor']:
|
||||
self.tls[traffic_light['id']] = {'color': traffic_light['color'],
|
||||
'switching_time': traffic_light['switchingTime'],
|
||||
'last_switch': datetime.utcnow()}
|
||||
|
||||
def setup_msg_queues(self):
|
||||
"""
|
||||
Setup the message queues for communicating with vehicles and traffic lights.
|
||||
A receiving and a sending queue for vehicles.
|
||||
A receiving queue for traffic lights.
|
||||
"""
|
||||
# spawn the vehicle related message broker channels
|
||||
for vin in self.vins:
|
||||
daf_channel = MBWrapper(exchange_name='vehicle_daf_{}'.format(vin), callback=self.handle_daf_receive)
|
||||
daf_channel.setup_receiver()
|
||||
self._daf_mbs[vin] = daf_channel
|
||||
|
||||
velocity_channel = MBWrapper(exchange_name='vehicle_velocity_{}'.format(vin))
|
||||
velocity_channel.setup_sender()
|
||||
self._velocity_mbs[vin] = velocity_channel
|
||||
|
||||
# spawn the traffic light related mb channels
|
||||
for tl in self.tls:
|
||||
tl_channel = MBWrapper(exchange_name='traffic_light_state_{}'.format(tl),
|
||||
callback=self.handle_tl_state_receive)
|
||||
tl_channel.setup_receiver()
|
||||
|
||||
def handle_daf_receive(self, pickle_binary):
|
||||
"""
|
||||
Gets the daf object's pickle binary dump.
|
||||
Unpickle, calculate new target velocity based on daf and current tl data and
|
||||
respond new target velocity for this vehicle.
|
||||
|
||||
:param pickle_binary: daf object pickle binary dump
|
||||
"""
|
||||
received_daf_object: DAF = pickle.loads(pickle_binary)
|
||||
loc = received_daf_object.gps_location
|
||||
print('Received DAF object: {}'.format(received_daf_object))
|
||||
|
||||
response = self._session.get(ENTITY_IDENT_URL + 'traffic_lights_geo',
|
||||
params={'lat': loc.latitude, 'lon': loc.longitude})
|
||||
traffic_lights_geo = response.json()
|
||||
current_vel = received_daf_object.velocity
|
||||
|
||||
target_vel = self._compute_velocity(traffic_lights_geo, current_vel)
|
||||
|
||||
response_channel = self._velocity_mbs[received_daf_object.vehicle_identification_number]
|
||||
print('Target velocity: {}'.format(target_vel))
|
||||
response_channel.send(pickle.dumps(
|
||||
TargetVelocity(vin=received_daf_object.vehicle_identification_number, target_velocity=target_vel,
|
||||
timestamp=datetime.utcnow())))
|
||||
|
||||
def handle_tl_state_receive(self, msg):
|
||||
"""
|
||||
Gets a dict full of traffic light state information:
|
||||
{
|
||||
'tlid': str,
|
||||
'color': TrafficLightColor,
|
||||
'last_switch': datetime
|
||||
}
|
||||
Updates internal information about available TLs
|
||||
"""
|
||||
tl_state: TrafficLightState = pickle.loads(msg)
|
||||
self.tls[tl_state.tlid]['color'] = tl_state.color
|
||||
self.tls[tl_state.tlid]['last_switch'] = tl_state.last_switch
|
||||
print(tl_state)
|
||||
|
||||
def _compute_velocity(self, traffic_lights_geo, current_vel):
|
||||
target_vel = 130 * SCALING
|
||||
|
||||
print('Nearest traffic lights: {}'.format(traffic_lights_geo['cursor']))
|
||||
for traffic_light in traffic_lights_geo['cursor']:
|
||||
# Should only ever contain one traffic light (the next in line)
|
||||
tl_id = traffic_light['id']
|
||||
distance = traffic_light['calculatedRange']
|
||||
switching_time = self.tls[tl_id]['switching_time'] / float(SCALING)
|
||||
# Time until next switch must be scaled accordingly
|
||||
next_switch_time = self.tls[tl_id]['last_switch'] + timedelta(seconds=switching_time)
|
||||
time_until_switch = (next_switch_time - datetime.utcnow()).total_seconds()
|
||||
print('Distance to TL: {}'.format(distance))
|
||||
print('Time until switch in seconds: {}'.format(time_until_switch))
|
||||
|
||||
if self.tls[tl_id]['color'] is TrafficLightColor.RED:
|
||||
speed_needed_max = (distance / float(time_until_switch)) * 3.6
|
||||
if speed_needed_max < 130 * SCALING:
|
||||
if current_vel < speed_needed_max:
|
||||
target_vel = current_vel
|
||||
else:
|
||||
target_vel = floor(speed_needed_max)
|
||||
|
||||
else:
|
||||
# Cannot make it on next green
|
||||
i = 2
|
||||
while speed_needed_max > 130 * SCALING:
|
||||
next_green_phase_start = time_until_switch + switching_time * i
|
||||
speed_needed_max = (distance / float(next_green_phase_start)) * 3.6
|
||||
i = i + 2
|
||||
target_vel = floor(speed_needed_max)
|
||||
|
||||
elif self.tls[tl_id]['color'] is TrafficLightColor.GREEN:
|
||||
# Check if we can reach TL in time
|
||||
speed_needed_min = (distance / float(time_until_switch)) * 3.6
|
||||
if speed_needed_min < 130 * SCALING:
|
||||
if current_vel < speed_needed_min:
|
||||
target_vel = 130 * SCALING
|
||||
else:
|
||||
target_vel = current_vel
|
||||
else:
|
||||
i = 1
|
||||
speed_needed_max = 132 * SCALING
|
||||
while speed_needed_max > 130 * SCALING:
|
||||
next_green_phase_start = time_until_switch + switching_time * i
|
||||
speed_needed_max = (distance / float(next_green_phase_start)) * 3.6
|
||||
i = i + 2
|
||||
target_vel = floor(speed_needed_max)
|
||||
else:
|
||||
print('Unknown Traffic Light Color!')
|
||||
return target_vel
|
||||
@@ -0,0 +1,3 @@
|
||||
flask
|
||||
geopy
|
||||
requests
|
||||
@@ -0,0 +1,54 @@
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from dse_shared_libs.mock.response import MyResponse
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
|
||||
from orchestrator import Orchestrator
|
||||
|
||||
|
||||
class TestOrchestrator(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.timestamp = datetime.datetime.utcnow()
|
||||
with patch('requests.sessions.Session.get', MyResponse):
|
||||
self.orc = Orchestrator()
|
||||
|
||||
def test_full_speed_if_nothing_in_range(self):
|
||||
tl_geo = {'cursor': []}
|
||||
current_vel = 55.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertEqual(130, target_vel)
|
||||
|
||||
def test_keep_speed_if_far_away_and_green(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.GREEN, 'switching_time': 1000, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1000}
|
||||
]}
|
||||
current_vel = 55.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertEqual(current_vel, target_vel)
|
||||
|
||||
def test_slow_down_if_passing_RED_not_possible(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100000, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1}
|
||||
]}
|
||||
current_vel = 130.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertNotEqual(current_vel, target_vel)
|
||||
self.assertEqual(0, target_vel)
|
||||
|
||||
def test_adjust_speed_to_get_over_on_green_if_currently_red(self):
|
||||
self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100, 'last_switch': self.timestamp}}
|
||||
tl_geo = {'cursor': [
|
||||
{'id': '1', 'calculatedRange': 1000}
|
||||
]}
|
||||
current_vel = 130.0
|
||||
|
||||
target_vel = self.orc._compute_velocity(tl_geo, current_vel)
|
||||
self.assertNotEqual(current_vel, target_vel)
|
||||
self.assertEqual(36.0, target_vel)
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM rabbitmq
|
||||
ENV PORT 5672
|
||||
ENV HOSTNAME rabbitmq
|
||||
EXPOSE 5672
|
||||
@@ -0,0 +1,3 @@
|
||||
build
|
||||
dist
|
||||
dse_shared_libs.egg-info
|
||||
@@ -0,0 +1,8 @@
|
||||
### Contains shared libraries like rabbitMQ wrapper
|
||||
* To have life changes (develop package only links to this folder): `pip install -e .`
|
||||
* To install as independent package: `pip install .`
|
||||
* Create installable package via `python setup.py sdist`
|
||||
|
||||
### Access shared libs via
|
||||
* `from dse_shared_libs.* import *`
|
||||
* e.g. `from dse_shared_libs.message_broker_wrapper import MBWrapper`
|
||||
@@ -0,0 +1,26 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import geopy.distance
|
||||
|
||||
|
||||
@dataclass
|
||||
class DAF:
|
||||
"""
|
||||
"Datenaufzeichnung für automatisiertes Fahren" (DAF), see 2021_DSE_Exercise_V1.0_Final.pdf page 7
|
||||
"""
|
||||
|
||||
vehicle_identification_number: str
|
||||
gps_location: geopy.Point
|
||||
velocity: float
|
||||
near_crash_event: bool
|
||||
timestamp: datetime
|
||||
|
||||
def to_dict(self):
|
||||
return {'vehicle_identification_number': self.vehicle_identification_number,
|
||||
'gps_location': {'latitude': self.gps_location.latitude,
|
||||
'longitude': self.gps_location.longitude
|
||||
},
|
||||
'velocity': self.velocity,
|
||||
'near_crash_event': self.near_crash_event,
|
||||
'timestamp': self.timestamp.timestamp()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import time
|
||||
from threading import Thread
|
||||
from typing import Union
|
||||
|
||||
import pika
|
||||
from pika.exceptions import AMQPConnectionError
|
||||
|
||||
|
||||
class MBWrapper:
|
||||
host: str
|
||||
exchange_type: str
|
||||
exchange_name: str
|
||||
callback: callable
|
||||
|
||||
_logger: Union['MBWrapper', None]
|
||||
|
||||
_type: str = None
|
||||
_connection: pika.BlockingConnection
|
||||
_channel: pika.BlockingConnection.channel
|
||||
|
||||
def __init__(self,
|
||||
host: str = 'rabbitmq',
|
||||
exchange_type: str = 'fanout',
|
||||
exchange_name: str = None,
|
||||
callback: callable = None,
|
||||
verbose: bool = False):
|
||||
"""
|
||||
Initializes a new Message Broker Wrapper (MBWrapper). Initialize this object afterwards with
|
||||
self.setup_sender() or self.setup_receiver() if messages will be published to or received from the queue.
|
||||
|
||||
If the exchange_name is not "logger", the MBWrapper will also initialize an sending queue which forwards
|
||||
EVERY request to this exchange, too. Therefore, every request routed via a MBWrapper is logged. Except if it is
|
||||
the logger MBWrapper. This one is initialized in the EventStore Service and receives all messages.
|
||||
|
||||
:param host: of running rabbitMQ instance
|
||||
:param exchange_type: rabbitMQ exchange type to use
|
||||
:param exchange_name: name of exchange
|
||||
:param callback: callable callback to execute if message receiver will be set up, can also be set later on.
|
||||
:param verbose: print handled messages to console
|
||||
"""
|
||||
assert exchange_name, 'Please define an exchange name'
|
||||
|
||||
# append a connection to the logging broker, if it isn't the logging broker itself
|
||||
if exchange_name != 'logger':
|
||||
self._logger = MBWrapper(exchange_name='logger')
|
||||
self._logger.setup_sender()
|
||||
else:
|
||||
self._logger = None
|
||||
|
||||
self.host = host
|
||||
self.exchange_type = exchange_type
|
||||
self.exchange_name = exchange_name
|
||||
self.callback = callback
|
||||
self.verbose = verbose
|
||||
|
||||
def print(self, *msg):
|
||||
"""
|
||||
Modified print which prints only to stout with self.verbose.
|
||||
:param msg: to print
|
||||
"""
|
||||
if self.verbose:
|
||||
print(*msg)
|
||||
|
||||
def setup_sender(self):
|
||||
"""
|
||||
Setup the MBWrapper as sender.
|
||||
"""
|
||||
assert self._type != 'receiver', 'MBWrapper is already a receiver. Use another MBWrapper.'
|
||||
self._type = 'sender'
|
||||
self._setup_channel()
|
||||
|
||||
def setup_receiver(self, callback: callable = None):
|
||||
"""
|
||||
Setup the MBWrapper as a receiver.
|
||||
A callback method which can handle the response bytes as input is mandatory.
|
||||
Set it here if not already done on init.
|
||||
"""
|
||||
if callback:
|
||||
self.callback = callback
|
||||
|
||||
assert self._type != 'sender', 'MBWrapper is already a sender. Use another MBWrapper.'
|
||||
assert self.callback, \
|
||||
'Please setup MBWrapper with "on response" self.callback which can handle a byte string as input.'
|
||||
|
||||
def consumer():
|
||||
"""
|
||||
If initialized as receiver: Consumer thread which waits for incoming messages, invokes self._receive
|
||||
"""
|
||||
self._setup_channel()
|
||||
result = self._channel.queue_declare(queue='', exclusive=True)
|
||||
queue_name = result.method.queue
|
||||
|
||||
self._channel.queue_bind(exchange=self.exchange_name, queue=queue_name)
|
||||
|
||||
self.print('[{}] Waiting for messages.'.format(self.exchange_name))
|
||||
|
||||
self._channel.basic_consume(
|
||||
queue=queue_name, on_message_callback=self._receive, auto_ack=True)
|
||||
|
||||
self._channel.start_consuming()
|
||||
|
||||
Thread(target=consumer).start()
|
||||
|
||||
def send(self, message: bytes):
|
||||
"""
|
||||
Send the message to the queue. Forward also to logger.
|
||||
|
||||
:param message: msg to send
|
||||
"""
|
||||
if type(message) is not bytes:
|
||||
message = str(message).encode()
|
||||
self._channel.basic_publish(exchange=self.exchange_name, routing_key='', body=message)
|
||||
self.print("[{}] Sent {}".format(self.exchange_name, message))
|
||||
|
||||
# has a logger IFF not 'logger' as exchange name -> forwards everything to logger exchange, too
|
||||
if self.__getattribute__('_logger'):
|
||||
# print('logging')
|
||||
self._logger.send(message)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the connection.
|
||||
"""
|
||||
self._connection.close()
|
||||
|
||||
def _setup_channel(self):
|
||||
"""
|
||||
Setup the rabbitMQ channel. Retry if not succeeded (Allow the dependencies to
|
||||
boot). Wait one second between retries.
|
||||
"""
|
||||
connect_succeeded = False
|
||||
while not connect_succeeded:
|
||||
try:
|
||||
self._connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=self.host))
|
||||
connect_succeeded = True
|
||||
except pika.exceptions.AMQPConnectionError:
|
||||
print('Could not connect, retry')
|
||||
time.sleep(1)
|
||||
|
||||
self._channel = self._connection.channel()
|
||||
self._channel.exchange_declare(exchange=self.exchange_name, exchange_type=self.exchange_type)
|
||||
|
||||
def _receive(self, ch, method, properties, body):
|
||||
"""
|
||||
Reduce complexity, only forward the message body to the callback method. The others are not necessary for our
|
||||
application example.
|
||||
"""
|
||||
self.callback(body)
|
||||
@@ -0,0 +1,10 @@
|
||||
class MyDate:
|
||||
timestamp = None
|
||||
|
||||
@classmethod
|
||||
def set_timestamp(cls, timestamp):
|
||||
cls.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
return cls.timestamp
|
||||
@@ -0,0 +1,43 @@
|
||||
class MyQueue:
|
||||
@property
|
||||
def queue(self):
|
||||
return None
|
||||
|
||||
|
||||
class MyMethod:
|
||||
method = MyQueue()
|
||||
|
||||
|
||||
class MyChannel:
|
||||
@staticmethod
|
||||
def exchange_declare(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def queue_declare(*args, **kwargs):
|
||||
return MyMethod
|
||||
|
||||
@staticmethod
|
||||
def queue_bind(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def basic_consume(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def basic_publish(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def start_consuming(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
class MyBlockingConnection:
|
||||
def __init__(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def channel():
|
||||
return MyChannel()
|
||||
@@ -0,0 +1,7 @@
|
||||
class MyResponse:
|
||||
def __init__(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def json(*args, **kwargs):
|
||||
return {'cursor': []}
|
||||
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class TargetVelocity:
|
||||
vin: str
|
||||
target_velocity: float
|
||||
timestamp: datetime
|
||||
|
||||
def to_dict(self):
|
||||
return {'vin': self.vin,
|
||||
'target_velocity': self.target_velocity,
|
||||
'timestamp': self.timestamp.timestamp()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TrafficLightColor(Enum):
|
||||
GREEN = 0
|
||||
RED = 1
|
||||
@@ -0,0 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from dse_shared_libs.traffic_light_color import TrafficLightColor
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrafficLightState:
|
||||
tlid: str
|
||||
color: TrafficLightColor
|
||||
last_switch: datetime
|
||||
|
||||
def to_dict(self):
|
||||
return {'tlid': self.tlid,
|
||||
'color': self.color.name,
|
||||
'last_switch': self.last_switch.timestamp()}
|
||||
@@ -0,0 +1,9 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(name='dse-shared-libs',
|
||||
version=1,
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
license='BSD License', # example licensez
|
||||
install_requires=['pika', 'geopy']
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM python:3.8-slim
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
ENV PORT 5003
|
||||
EXPOSE 5003
|
||||
ENTRYPOINT [ "python" ]
|
||||
CMD [ "x_way_server.py" ]
|
||||
@@ -0,0 +1,6 @@
|
||||
# Super Lightweight Flask Web Server
|
||||
* `pip install requirements.txt`
|
||||
* `python x_way.py`
|
||||
* `http://127.0.0.1:5000` -> `Hello World`
|
||||
* `http://127.0.0.1:5000/api1` -> `api1`
|
||||
* `http://127.0.0.1:5000/api2` -> `api2`
|
||||
@@ -0,0 +1,6 @@
|
||||
flask==1.1.4
|
||||
Flask-Cors
|
||||
requests
|
||||
Flask-PyMongo
|
||||
jsonify
|
||||
flask_restx
|
||||
@@ -0,0 +1,96 @@
|
||||
import flask
|
||||
import flask_restx
|
||||
import requests
|
||||
from bson import json_util
|
||||
from flask import Flask, jsonify
|
||||
from flask_cors import CORS
|
||||
from flask import request
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
api_bp = flask.Blueprint("api", __name__, url_prefix="/api/v1")
|
||||
|
||||
API = flask_restx.Api(api_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
NAMESPACE = API.namespace("resources")
|
||||
|
||||
ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
|
||||
EVENT_STORE_URL = 'http://eventstore:5001/api/keys/'
|
||||
|
||||
|
||||
@NAMESPACE.route('/car_events', doc={'description': 'Get most current car event for specific car'})
|
||||
class CarsEvents(flask_restx.Resource):
|
||||
@NAMESPACE.doc(params={'vin': {'description': 'Vehicle Identifier Number of car',
|
||||
'type': 'String', 'default': '5GZCZ43D13S812715'}})
|
||||
@NAMESPACE.response(200, 'Success')
|
||||
def get(self):
|
||||
vin = request.args.get('vin')
|
||||
|
||||
try:
|
||||
response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/0/')
|
||||
cars = json.loads(response.text)
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the EVENT_STORE_URL running and reachable?")
|
||||
raise e
|
||||
|
||||
return jsonify(cars)
|
||||
|
||||
|
||||
@NAMESPACE.route('/traffic_light_events', doc={'description': 'Get most current traffic light event'
|
||||
'for specific traffic light'})
|
||||
class TrafficLightEvents(flask_restx.Resource):
|
||||
@NAMESPACE.doc(params={'id': {'description': 'Unique ID of traffic light',
|
||||
'type': 'int', 'default': '1'}})
|
||||
@NAMESPACE.response(200, 'Success')
|
||||
def get(self):
|
||||
id = request.args.get('id')
|
||||
|
||||
try:
|
||||
response = requests.get(EVENT_STORE_URL + 'TL:' + id + '/0/')
|
||||
traffic_lights = json.loads(response.text)
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the EVENT_STORE_URL running and reachable?")
|
||||
raise e
|
||||
|
||||
return jsonify(traffic_lights)
|
||||
|
||||
|
||||
@NAMESPACE.route('/cars', doc={'description': 'Get entity details for all cars'})
|
||||
class Car(flask_restx.Resource):
|
||||
@NAMESPACE.response(200, 'Success')
|
||||
def get(self):
|
||||
|
||||
try:
|
||||
response = requests.get(ENTITY_IDENT_URL + 'cars')
|
||||
cars = response.json()['cursor']
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the entity_ident_server running and reachable?")
|
||||
raise e
|
||||
|
||||
return jsonify(cars)
|
||||
|
||||
|
||||
@NAMESPACE.route('/traffic_lights', doc={'description': 'Get entity details for all traffic lights'})
|
||||
class TrafficLights(flask_restx.Resource):
|
||||
|
||||
@NAMESPACE.response(200, 'Success')
|
||||
def get(self):
|
||||
|
||||
try:
|
||||
response = requests.get(ENTITY_IDENT_URL + 'traffic_lights')
|
||||
traffic_lights = response.json()['cursor']
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print("Is the entity_ident_server running and reachable?")
|
||||
raise e
|
||||
|
||||
return jsonify(traffic_lights)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run('0.0.0.0', 5004)
|
||||
Reference in New Issue
Block a user