Merge branch 'master' of gitlab.com:kranklyboy/webapptemplate

This commit is contained in:
Tobias Eidelpes 2021-06-13 12:45:56 +02:00
commit 6806f1f5dd
20 changed files with 284 additions and 144 deletions

View File

@ -6,7 +6,6 @@ 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 {WebsocketService} from './services/websocket.service';
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
import {environment} from '../environments/environment';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@ -39,7 +38,6 @@ import {AgmCoreModule, GoogleMapsAPIWrapper} from '@agm/core';
providers: [
GoogleMapsAPIWrapper,
RestService,
WebsocketService,
{
provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true
},

View File

@ -1,5 +1,3 @@
html, body {
height: 100%;
padding: 0;
margin: 0;
#map{
height : 100vh; width: 100%; padding:0px; margin: 0px;
}

View File

@ -1,8 +1,26 @@
<button (click)="toggleMarker()">Drive!</button>
<agm-map *ngIf="traffic_light_markers.length > 0" id="map" #map [zoom]="zoom" [latitude]="center.lat" [longitude]="center.lng" style="height: 900px; width: 1200px">
<!--<agm-marker *ngFor="let m of markers" [iconUrl]="m.iconUrl" [visible]="m.visible" [latitude]="m.lat" [longitude]="m.lng" >
</agm-marker>-->
<agm-marker *ngFor="let m of traffic_light_markers" [iconUrl]="m.iconUrl" [latitude]="m.lat" [longitude]="m.lng" >
<agm-map disableDefaultUI="true" id="map" #map [zoom]="zoom" [latitude]="center.lat" [longitude]="center.lng">
<agm-marker (markerClick)="openInfoWindow(m.value.vin)" [openInfoWindow]="true"
*ngFor="let m of car_markers | keyvalue" [iconUrl]="m.value.iconUrl"
[latitude]="m.value.lat" [longitude]="m.value.lng" [animation]="(m.value.near_crash_event)?'BOUNCE':''">
<agm-info-window style="opacity: 0.5!important;" (infoWindowClose)="closeInfoWindow(m.value.vin)"
[isOpen]="isInfoWindowOpen(m.value.vin)" [latitude]="m.value.lat"
[longitude]="m.value.lng">
<p>VIN: {{m.value.vin}}</p>
<p>OEM: {{m.value.oem}}</p>
<p>Model Type: {{m.value.modelType}}</p>
<p>Velocity: {{m.value.velocity}}</p>
<p>Timestamp: {{m.value.timestamp}}</p>
<p>NCE: {{m.value.near_crash_event}}</p>
</agm-info-window>
</agm-marker>
<agm-marker *ngFor="let m of traffic_light_markers | keyvalue" [iconUrl]="m.value.iconUrl"
[latitude]="m.value.lat" [longitude]="m.value.lng">
<agm-info-window>
<p>Id: {{m.value.id}}</p>
<p>Switching Time: {{m.value.switchingTime}}</p>
<p>Range: {{m.value.range}}</p>
<p>Color: {{m.value.color}}</p>
<p>Last Switch: {{m.value.last_switch}}</p>
</agm-info-window>
</agm-marker>
</agm-map>

View File

@ -1,7 +1,9 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {AgmMap, AgmMarker} from '@agm/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';
@Component({
selector: 'app-landing',
@ -22,72 +24,137 @@ export class LandingComponent implements OnInit {
zoom = 14;
center = {lat: 47.90620, lng: 16.20785};
// Test Data
markers = [
{
Id: 1,
name: 'Car-1',
lat: 47.89053,
lng: 16.20703,
visible: true,
iconUrl: 'assets/pictures/car.png'
},
{
Id: 2,
name: 'Car-2',
lat: 47.89853,
lng: 16.20703,
visible: true,
iconUrl: 'assets/pictures/car.png'
},
];
traffic_light_markers = [];
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() {
let traffic_lights = [];
this.getTrafficLights();
this.getCars();
}
getCarEvents(vin) {
this.timeInterval = interval(1000)
.pipe(
startWith(0),
switchMap(() => this.restService.getCarEvents(vin))
).subscribe((data: any) => {
const carEvent = data.body['cursor'];
const car = this.car_markers.get(vin);
car['velocity'] = carEvent['velocity'];
car['timestamp'] = carEvent['timestamp'];
car['near_crash_event'] = carEvent['near_crash_event'];
if (car['near_crash_event']) {
car['iconUrl'] = this.CAR_IMAGE_NCE;
this.playCrashSound();
} else {
car['iconUrl'] = this.CAR_IMAGE;
}
car['lat'] = carEvent['gps_location']['latitude'];
car['lng'] = carEvent['gps_location']['longitude'];
this.car_markers.set(car['vin'], car);
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded traffic light events');
}
);
}
getTrafficLightEvents(tlid) {
this.timeInterval = interval(1000)
.pipe(
startWith(0),
switchMap(() => this.restService.getTrafficLightEvents(tlid))
).subscribe(
(data: any) => {
const traffic_light_event = data.body['cursor'];
const traffic_light = this.traffic_light_markers.get(traffic_light_event['tlid']);
traffic_light['color'] = traffic_light_event['color'];
traffic_light['last_switch'] = traffic_light_event['last_switch'];
traffic_light['iconUrl'] = this.trafficLightToImage(traffic_light['color']);
this.traffic_light_markers.set(traffic_light_event['tlid'], traffic_light);
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded traffic light events');
}
);
}
getTrafficLights() {
this.restService.getTrafficLights().subscribe(
(data: any) => {
traffic_lights = data;
console.log(data['cursor']);
for (const traffic_light of data['cursor']) {
traffic_light['iconUrl'] = 'assets/pictures/traffic_light_red.png';
traffic_light['iconUrl'] = this.trafficLightToImage(traffic_light['color']);
traffic_light['lat'] = traffic_light['location'][1];
traffic_light['lng'] = traffic_light['location'][0];
console.log(traffic_light);
this.traffic_light_markers.push(traffic_light);
this.traffic_light_markers.set(traffic_light['id'], traffic_light);
}
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded traffic lights');
for (const value of this.traffic_light_markers.values()) {
this.getTrafficLightEvents(value['id']);
}
}
);
}
getDelta(source, destination, steps) {
return {
lat: (destination.lat - source.lat) / steps,
lng: (destination.lng - source.lng) / steps
};
getCars() {
this.restService.getCars().subscribe(
(data: any) => {
for (const car of data['cursor']) {
car['iconUrl'] = 'assets/pictures/car.png';
this.car_markers.set(car['vin'], car);
}
toggleMarker() {
const delta = this.getDelta(this.markers[0], this.markers[1], 100);
this.move(this.markers[0], delta, 0, 10, 100);
},
err => this.logger.error(err),
() => {
this.logger.debug('loaded cars');
console.log(this.car_markers);
for (const value of this.car_markers.values()) {
this.getCarEvents(value['vin']);
}
}
);
}
move(source, delta, counter, delay, steps) {
source.lat += delta.lat;
source.lng += delta.lng;
if (counter !== steps) {
counter++;
setTimeout(this.move.bind(this), delay, source, delta, counter, delay, steps);
}
private trafficLightToImage(trafficLight) {
return (trafficLight === 'RED') ? this.TL_RED_IMAGE : this.TL_GREEN_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();
}
}

View File

@ -32,7 +32,7 @@ export class InterceptorService implements HttpInterceptor {
}
});
this.logger.debug('Interceptor works');
//this.logger.debug('Interceptor works');
// pipe the response observable
return next.handle(req).pipe(

View File

@ -2,7 +2,6 @@ import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../environments/environment';
import {Observable} from 'rxjs';
@Injectable()
export class RestService {
@ -14,7 +13,20 @@ export class RestService {
) {
}
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');
}
}

View File

@ -1,52 +0,0 @@
import {Injectable} from '@angular/core';
import {NGXLogger} from 'ngx-logger';
import {fromEvent, interval, Observable} from 'rxjs';
import {environment} from '../../environments/environment';
import {WSEvents} from '../interfaces/interface';
@Injectable({
providedIn: 'root'
})
export class WebsocketService {
private wsEndpoint = environment.ws_location + ':' + environment.ws_port + '/test-ws-endpoint/';
private readonly ws: WebSocket;
private wsEvents: WSEvents;
constructor(private logger: NGXLogger) {
this.logger.debug('Initiating ws connection on', this.wsEndpoint);
this.ws = new WebSocket(this.wsEndpoint);
interval(5000).subscribe(() => {
// continuously check if the connection is still open
if (this.ws.readyState !== WebSocket.OPEN) {
this.logger.error('Lost websocket connection ...', this.ws);
}
});
}
wsTestCall(msg: string): WSEvents {
this.logger.debug(
'Performing ws test call',
'current ws ready state ==', this.ws.readyState + ',',
'expected == 1 == open');
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(msg);
} else {
return undefined;
}
if (!this.wsEvents) {
this.wsEvents = {
message: fromEvent(this.ws, 'message'),
error: fromEvent(this.ws, 'error'),
close: fromEvent(this.ws, 'close')
};
}
return this.wsEvents;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

View File

@ -2,8 +2,7 @@ import {NgxLoggerLevel} from 'ngx-logger';
export const environment = {
production: true,
location: window.location.hostname,
location: 'xway',
port: 5004,
ws_url_root: 'ws://' + window.location.hostname + ':' + window.location.port + '/',
log_level: NgxLoggerLevel.WARN,
};

View File

@ -6,10 +6,8 @@ import {NgxLoggerLevel} from 'ngx-logger';
export const environment = {
production: false,
location: 'xwayserver',
location: 'xway',
port: 5004,
ws_location: 'ws://127.0.0.1',
ws_port: 8000,
log_level: NgxLoggerLevel.DEBUG,
};

View File

@ -4,6 +4,11 @@
@import '~bootstrap/dist/css/bootstrap.min.css';
body {
width: 95%;
margin: auto;
width: 100%;
margin: 0;
padding: 0;
}
.gm-style-iw-a {
opacity: 0.8 !important;
}

View File

@ -4,20 +4,20 @@
"location": [16.20719, 47.89584],
"range": 542,
"switchingTime": 15,
"initialColor": "RED"
"color": "RED"
},
{
"id": "2",
"location": [16.20814, 47.90937],
"range": 725,
"switchingTime": 20,
"initialColor": "GREEN"
"color": "GREEN"
},
{
"id": "3",
"location": [16.20917, 47.92703],
"range": 910,
"switchingTime": 25,
"initialColor": "RED"
"color": "RED"
}
]

View File

@ -39,6 +39,14 @@ class TrafficLight:
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

View File

@ -70,6 +70,19 @@ class Vehicle:
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
@ -85,6 +98,10 @@ class Vehicle:
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
"""
if self._nce_possible and not self._nce_happened:
@ -115,10 +132,13 @@ class Vehicle:
@property
def daf(self):
"""
:return: "Datenaufzeichnung für automatisiertes Fahren" (DAF) object
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
# 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,
@ -133,10 +153,10 @@ class Vehicle:
@property
def gps_location(self):
"""
Update self.gps_location with given speed in km/h and the driven time in seconds
:param velocity: in km/h
:param time: in seconds
:param bearing: direction in degrees: 0=N, 90=E, 180=S, 270=W
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
@ -165,6 +185,9 @@ class Vehicle:
@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):
@ -211,12 +234,16 @@ class Vehicle:
@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
Will be invoked if new target velocity message received from orchestrator.
:param response: pickled TargetVelocity object
"""
response: TargetVelocity = pickle.loads(response)

View File

@ -52,7 +52,7 @@ class Orchestrator:
self.vins.append(car['vin'])
for traffic_light in traffic_lights['cursor']:
self.tls[traffic_light['id']] = {'color': traffic_light['initialColor'],
self.tls[traffic_light['id']] = {'color': traffic_light['color'],
'switching_time': traffic_light['switchingTime'],
'last_switch': datetime.now()}

View File

@ -24,6 +24,20 @@ class MBWrapper:
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
@ -40,22 +54,37 @@ class MBWrapper:
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):
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():
"""
Consumer thread which waits for incoming messages, invokes self._receive
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)
@ -73,6 +102,11 @@ class MBWrapper:
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)
@ -84,9 +118,16 @@ class MBWrapper:
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:
@ -102,6 +143,7 @@ class MBWrapper:
def _receive(self, ch, method, properties, body):
"""
Reduce complexity, only forward the message body to the callback method
Reduce complexity, only forward the message body to the callback method. The others are not necessary for our
application example.
"""
self.callback(body)

View File

@ -1,3 +1,5 @@
flask
Flask-Cors
requests
Flask-PyMongo
jsonify

View File

@ -12,18 +12,12 @@ ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/'
EVENT_STORE_URL = 'http://eventstore:5001/api/keys/'
@app.route('/')
def hello_world():
return 'Hello World'
@app.route('/api/v1/resources/car_events', methods=['GET'])
def get_cars_events():
vin = request.args.get('vin')
try:
response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/')
response = requests.get(EVENT_STORE_URL + 'DAF:' + vin + '/0/')
cars = json.loads(response.text)
except requests.exceptions.ConnectionError as e:
@ -32,9 +26,24 @@ def get_cars_events():
return json_util.dumps({'cursor': cars})
@app.route('/api/v1/resources/traffic_light_events', methods=['GET'])
def get_traffic_light_events():
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 json_util.dumps({'cursor': traffic_lights})
@app.route('/api/v1/resources/cars', methods=['GET'])
def get_cars():
try:
response = requests.get(ENTITY_IDENT_URL + 'cars')
cars = response.json()['cursor']
@ -48,7 +57,6 @@ def get_cars():
@app.route('/api/v1/resources/traffic_lights', methods=['GET'])
def get_traffic_lights():
try:
response = requests.get(ENTITY_IDENT_URL + 'traffic_lights')
traffic_lights = response.json()['cursor']

View File

@ -91,3 +91,13 @@ services:
- entityident
- orchestration
- eventstore
controlcenter:
build:
context: ../components/control_center
dockerfile: Dockerfile
expose:
- 80
ports:
- 80:80
depends_on:
- xway