feat: added broken map for supporting multiple tracks (will likely get removed) and added colorless rendering for tracks in point cloud
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 47s

This commit is contained in:
steev 2025-01-13 00:49:08 +01:00
parent 8a59bbbea7
commit df1795c0bd
11 changed files with 147263 additions and 175 deletions

51
app.py
View File

@ -53,6 +53,18 @@ def getTrack():
if ("start" in request.args and "end" in request.args) or ("id" in request.args): if ("start" in request.args and "end" in request.args) or ("id" in request.args):
if "start" in request.args and "end" in request.args: if "start" in request.args and "end" in request.args:
if "asMap" in request.args:
# get tracks by filter
start = request.args["start"]
end = request.args["end"]
# Die GeoJSON-Daten aus der Datenbank abrufen
geojson_data = gpxHandler.getTracksInTimeWithGeoData(start, end)
app.logger.debug(f"returned track {geojson_data}")
# Die GeoJSON-Daten als JSON zurückgeben
return jsonify(geojson_data)
# get tracks by filter # get tracks by filter
start = request.args["start"] start = request.args["start"]
end = request.args["end"] end = request.args["end"]
@ -78,12 +90,36 @@ def getTrack():
return f"error {e}", 500 return f"error {e}", 500
else: else:
try: try:
# gets all tracks as list tracks = gpxHandler.getTracks()
return gpxHandler.getTracks() if len(tracks) > 0:
# gets all tracks as list
return tracks, 200
else:
return [], 200
except Exception as e: except Exception as e:
app.logger.debug(f"fetching all tracks failed with error {e}") app.logger.debug(f"fetching all tracks failed with error {e}")
return f"error {e}", 500 return f"error {e}", 500
@app.route("/track/meta", methods=['GET'])
@cross_origin()
def getTrackMeta():
app.logger.debug(f"found arguments {request.args}")
if "id" in request.args:
# get track by id
trackID = int(request.args["id"])
try:
app.logger.debug(f"Request args: {request.args}")
app.logger.debug(f"track id {trackID}")
track = gpxHandler.getTrackMeta(trackID)
app.logger.debug(f"returned track {track}")
return jsonify(track), 200
except Exception as e:
app.logger.debug(f"fetching track failed with error {e}")
return f"error {e}", 500
@app.route("/driver", methods=['GET', 'POST']) @app.route("/driver", methods=['GET', 'POST'])
@cross_origin() @cross_origin()
@ -103,7 +139,8 @@ def handleDriverRoute():
try: try:
drivers = driverHandler.getDrivers() drivers = driverHandler.getDrivers()
return drivers, 200 if len(drivers) > 0:
return drivers, 200
except Exception as e: except Exception as e:
app.logger.debug(f"getting drivers failed with error {e}") app.logger.debug(f"getting drivers failed with error {e}")
return "error" + " " + str(e), 500 return "error" + " " + str(e), 500
@ -163,11 +200,17 @@ def handleVehicleRoute():
if "name" not in data: if "name" not in data:
return "missing name", 400 return "missing name", 400
licenseplate = ""
if "licensePlate" not in data:
licenseplate = "N/A"
else:
licenseplate = data["licensePlate"]
name = data["name"] name = data["name"]
# handle creating vehicle # handle creating vehicle
try: try:
vehicle = vehicleHandler.createVehicle(name) vehicle = vehicleHandler.createVehicle(name, licenseplate)
return jsonify({"id": vehicle.id, "name": vehicle.name}), 200 return jsonify({"id": vehicle.id, "name": vehicle.name}), 200
except Exception as e: except Exception as e:

View File

@ -17,10 +17,10 @@ services:
POSTGRES_USER: example POSTGRES_USER: example
POSTGRES_PASSWORD: example POSTGRES_PASSWORD: example
POSTGRES_DB: geotrack POSTGRES_DB: geotrack
web: # web:
build: . # build: .
ports: # ports:
- "8000:5000" # - "8000:5000"
adminer: adminer:
image: adminer image: adminer
restart: always restart: always

View File

@ -6,7 +6,6 @@ from sqlalchemy.exc import OperationalError
Base = declarative_base() Base = declarative_base()
# TODO: build license plate into car api construct and rebuild api to fit new data
# Funktion zur Herstellung einer Verbindung zur Datenbank # Funktion zur Herstellung einer Verbindung zur Datenbank
def db_connect(): def db_connect():
try: try:
@ -30,20 +29,23 @@ def create_table(engine):
except Exception as e: except Exception as e:
print(f"Fehler bei der Tabellenerstellung: {e}") print(f"Fehler bei der Tabellenerstellung: {e}")
# Track-Tabelle
class Track(Base): class Track(Base):
__tablename__ = 'track' __tablename__ = 'track'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
trackName = Column(String(200), nullable=True) trackName = Column(String(200), nullable=True)
vehicle_id = Column(Integer, ForeignKey('vehicle.id'), nullable=False, default=1) vehicle_id = Column(Integer, ForeignKey('vehicle.id'), nullable=False)
driver_id = Column(Integer, ForeignKey('driver.id'), nullable=False, default=1) driver_id = Column(Integer, ForeignKey('driver.id'), nullable=False)
date = Column(Date, nullable=True) date = Column(Date, nullable=True)
distance = Column(Float, nullable=False, default=0) distance = Column(Float, nullable=False, default=0.0)
speed = Column(Float, nullable=False, default=0) speed = Column(Float, nullable=False, default=0.0)
driver = relationship("Driver", backref="vehicle_tracks") # 'vehicle_tracks' als backref # Beziehungen
vehicle = relationship("Vehicle", backref="driver_tracks") # 'driver_tracks' als backref driver = relationship("Driver", back_populates="tracks", foreign_keys=[driver_id])
waypoints = relationship('Waypoint', backref='track', lazy=True) vehicle = relationship("Vehicle", back_populates="tracks", foreign_keys=[vehicle_id])
waypoints = relationship('Waypoint', back_populates='track', lazy=True)
# Waypoint-Tabelle
class Waypoint(Base): class Waypoint(Base):
__tablename__ = 'waypoint' __tablename__ = 'waypoint'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
@ -54,16 +56,24 @@ class Waypoint(Base):
time = Column(DateTime, nullable=True) time = Column(DateTime, nullable=True)
track_id = Column(Integer, ForeignKey('track.id'), nullable=False) track_id = Column(Integer, ForeignKey('track.id'), nullable=False)
# Beziehung zu Track
track = relationship("Track", back_populates="waypoints")
# Driver-Tabelle
class Driver(Base): class Driver(Base):
__tablename__ = 'driver' __tablename__ = 'driver'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
tracks = relationship("Track", back_populates="driver") # Beziehung von Track -> Driver
# Beziehung zu Tracks
tracks = relationship("Track", back_populates="driver", overlaps="vehicle_tracks")
# Vehicle-Tabelle
class Vehicle(Base): class Vehicle(Base):
__tablename__ = 'vehicle' __tablename__ = 'vehicle'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
tracks = relationship("Track", back_populates="vehicle") # Beziehung von Track -> Vehicle licenseplate = Column(String, nullable=True)
# Beziehung zu Tracks
tracks = relationship("Track", back_populates="vehicle", overlaps="driver_tracks")

View File

@ -2,7 +2,7 @@ import datetime
import gpxpy import gpxpy
import gpxpy.gpx import gpxpy.gpx
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from geojson import Feature, LineString from geojson import Feature, LineString, FeatureCollection
from geopy.distance import geodesic from geopy.distance import geodesic
from errors.NotFoundException import NotFoundError from errors.NotFoundException import NotFoundError
@ -92,6 +92,31 @@ class GPXHandler:
feature = Feature(geometry=LineString(coordinates)) feature = Feature(geometry=LineString(coordinates))
return feature return feature
# handles getting all infos of a track from the database
def getTrackMeta(self, trackID):
track = self.__dbSession.query(Track).filter_by(id=trackID).first()
if not track:
raise NotFoundError(f"track with id {trackID} not found", errors=[])
trackObject = {
"id": track.id,
"name": track.trackName,
"driver": {
"id": track.driver.id,
"name": track.driver.name
} if track.driver else None,
"vehicle": {
"id": track.vehicle.id,
"name": track.vehicle.name,
"licenseplate": track.vehicle.licenseplate
} if track.vehicle else None,
"distance": track.distance,
"time": track.date,
}
return trackObject
# grabs only the tracks from the database and returns them as json object # grabs only the tracks from the database and returns them as json object
def getTracks(self): def getTracks(self):
tracks = self.__dbSession.query(Track).all() tracks = self.__dbSession.query(Track).all()
@ -100,23 +125,39 @@ class GPXHandler:
{ {
"id": track.id, "id": track.id,
"name": track.trackName, "name": track.trackName,
# "driver": { "time": track.date,
# "id": track.driver.id,
# "name": track.driver.name
# } if track.driver else None,
# "vehicle": {
# "id": track.vehicle.id,
# "name": track.vehicle.name
# } if track.vehicle else None,
# "distance": track.distance,
# "startTime": track.start.isoformat() if track.start else None,
# "endTime": track.end.isoformat() if track.end else None,
} }
for track in tracks # iterates all tracks and appends them to the list for track in tracks # iterates all tracks and appends them to the list
] ]
return track_list return track_list
def getTracksInTimeWithGeoData(self, start, end):
# Alle Tracks in der Zeitspanne abfragen
tracks = self.__dbSession.query(Track).filter(Track.date.between(start, end)).all()
# Eine Liste von GeoJSON-Features für alle Tracks
features = []
# Für jedes Track-Objekt die Waypoints abfragen und die GeoJSON-Daten generieren
for track in tracks:
# Waypoints für das Track laden
waypoints = track.waypoints # track.waypoints ist bereits korrekt verknüpft
# Waypoints in GeoJSON-kompatible Koordinaten umwandeln
coordinates = [(wp.lon, wp.lat) for wp in waypoints]
# LineString Feature für das Track erstellen
feature = Feature(geometry=LineString(coordinates))
features.append(feature)
# Ein FeatureCollection erstellen, das alle Track-Features enthält
feature_collection = FeatureCollection(features)
# GeoJSON zurückgeben, das von Leaflet verarbeitet werden kann
return feature_collection
def getTracksInTime(self, start, end): def getTracksInTime(self, start, end):
tracks = self.__dbSession.query(Track).filter(Track.date.between(start, end)).all() tracks = self.__dbSession.query(Track).filter(Track.date.between(start, end)).all()
@ -134,8 +175,7 @@ class GPXHandler:
"name": track.vehicle.name "name": track.vehicle.name
} if track.vehicle else None, } if track.vehicle else None,
"distance": track.distance, "distance": track.distance,
"startTime": track.start.isoformat() if track.start else None, "time": track.date,
"end_time": track.end.isoformat() if track.end else None,
} }
for track in tracks # iterates all tracks and appends them to the list for track in tracks # iterates all tracks and appends them to the list
] ]

View File

@ -10,11 +10,11 @@ class VehicleHandler:
pass pass
# handles creating a vehicle and storing it in the database # handles creating a vehicle and storing it in the database
def createVehicle(self, name:str) -> Vehicle: def createVehicle(self, name:str, licenseplate:str) -> Vehicle:
if not name: if not name:
raise ValueError("name is empty") raise ValueError("name is empty")
vehicle = Vehicle(name=name) vehicle = Vehicle(name=name, licenseplate=licenseplate)
self.dbSession.add(vehicle) self.dbSession.add(vehicle)
self.dbSession.commit() self.dbSession.commit()
@ -34,7 +34,8 @@ class VehicleHandler:
driverList = [ driverList = [
{ {
"id": vehicle.id, "id": vehicle.id,
"name": vehicle.name "name": vehicle.name,
"licensePlate": vehicle.licenseplate
} }
# iterates all drivers and appends them to the list # iterates all drivers and appends them to the list
for vehicle in vehicles for vehicle in vehicles

146593
uploads/AA_WITAA333_001.gpx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,35 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, PropType, ref, watch } from 'vue'; import { defineComponent, onMounted, PropType, Ref, ref, watch } from 'vue';
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import L from "leaflet"; import L from "leaflet";
type Track = {
id:number
driver:string
vehicle:{
name: string
licenseplate: string
}
distance:number
}
export default defineComponent({ export default defineComponent({
name: 'Map', name: 'Map',
props: { props: {
geoJsonData: { geoJsonData: {
type: Object as PropType<any>, type: Object as PropType<any>,
required: true required: true
} },
multiple: {
type: Boolean,
required: true,
},
}, },
setup(props) { setup(props) {
const mapDiv = ref<HTMLElement | null>(null); // ref to map container element const mapDiv = ref<HTMLElement | null>(null); // ref to map container element
const mapInstance = ref<any>(null); // reference for the Leaflet map const mapInstance = ref<any>(null); // reference for the Leaflet map
const track:Ref<Track> = ref({id: 1, driver: "N/A", vehicle: {name: "N/A", licenseplate: "N/A"}, distance: 0.0})
const initializeMap = () => { const initializeMap = () => {
if (mapDiv.value) { if (mapDiv.value) {
@ -32,6 +48,33 @@ export default defineComponent({
initializeMap(); initializeMap();
}); });
const getTrackMeta = async (id:number) => {
const headers: Headers = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Accept', 'application/json')
const request: RequestInfo = new Request(`http://localhost:5000/track/meta?id=${id}`, {
method: "GET",
headers: headers
})
var response = await fetch(request)
// make sure the request was successfull
if (response.ok) {
var jsonBody = await response.json()
track.value = {
id: jsonBody["id"],
driver: jsonBody["driver"]["name"],
vehicle: {
name: jsonBody["vehicle"]["name"],
licenseplate: jsonBody["vehicle"]["licenseplate"] ?? "N/A"
},
distance: jsonBody["distance"]
}
}
}
// Beobachte geoJsonData und füge sie der Karte hinzu, wenn sie sich ändert // Beobachte geoJsonData und füge sie der Karte hinzu, wenn sie sich ändert
watch(() => props.geoJsonData, (newData) => { watch(() => props.geoJsonData, (newData) => {
if (newData) { if (newData) {
@ -39,6 +82,9 @@ export default defineComponent({
// Stelle sicher, dass die Karte initialisiert wurde // Stelle sicher, dass die Karte initialisiert wurde
if (mapInstance.value) { if (mapInstance.value) {
getTrackMeta(1)
console.log(mapInstance.value)
const geoJsonLayer = L.geoJSON(newData).addTo(mapInstance.value); const geoJsonLayer = L.geoJSON(newData).addTo(mapInstance.value);
const bounds = geoJsonLayer.getBounds(); const bounds = geoJsonLayer.getBounds();
@ -50,7 +96,8 @@ export default defineComponent({
}); });
return { return {
mapDiv, // Gebe mapDiv zurück, damit es im Template verwendet wird mapDiv,
track
}; };
}, },
}); });
@ -68,16 +115,18 @@ export default defineComponent({
<th>track name</th> <th>track name</th>
<th>driver</th> <th>driver</th>
<th>vehicle</th> <th>vehicle</th>
<th>distance Color</th> <th>License Plate</th>
<th>distance</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- row 1 --> <!-- row 1 -->
<tr class="bg-base-200"> <tr class="bg-base-200">
<th>1</th> <th>{{ track.id }}</th>
<td>Cy Ganderton</td> <td>{{ track.driver }}</td>
<td>Quality Control Specialist</td> <td>{{ track.vehicle.name }}</td>
<td>Blue</td> <td>{{ track.vehicle.licenseplate }}</td>
<td>{{ Math.round((track.distance / 1000)*100)/100}}KM</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -85,16 +134,14 @@ export default defineComponent({
</template> </template>
<style scoped> <style scoped>
/* Verhindert, dass die Karte über den Bildschirm hinausgeht */
#mapDiv { #mapDiv {
width: 100vw; /* Volle Breite des Viewports */ width: 100vw;
height: 70vh; /* Volle Höhe des Viewports */ height: 70vh;
margin: 0; /* Keine Margen */ margin: 0;
padding: 0; /* Keine Auffüllung */ padding: 0;
overflow: hidden; /* Verhindert ungewolltes Überlaufen */ overflow: hidden;
} }
/* Optional: Globale Styles für den Body und html, um das Layout zu normalisieren */
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -0,0 +1,141 @@
<script lang="ts">
import { defineComponent, onMounted, PropType, Ref, ref, watch } from 'vue';
import "leaflet/dist/leaflet.css";
import L from "leaflet";
type Track = {
id: number;
driver: string;
vehicle: {
name: string;
licenseplate: string;
};
distance: number;
};
export default defineComponent({
name: 'MapMultiple',
props: {
geoJsonData: {
type: Object as PropType<any>,
required: true
},
},
setup(props) {
const mapDiv = ref<HTMLElement | null>(null); // ref to map container element
const mapInstance = ref<any>(null); // reference for the Leaflet map
const tileLayer = ref<any>(null); // reference for the tile layer
const geoJsonLayer = ref<any>(null); // reference for the geoJSON layer
const tracks: Ref<Track[]> = ref([]);
const initializeMap = () => {
if (mapDiv.value) {
mapInstance.value = L.map(mapDiv.value, {
center: [51.4819, 7.2162], // Beispielkoordinaten
zoom: 13,
});
// Hinzufügen von OpenStreetMap-Kacheln und speichern des Layers in einer Variable
tileLayer.value = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {}).addTo(mapInstance.value);
}
};
const clearGeoJsonLayer = () => {
// Entferne nur den GeoJSON Layer, nicht den Tile Layer
if (geoJsonLayer.value) {
mapInstance.value.removeLayer(geoJsonLayer.value);
}
};
onMounted(() => {
console.log(props.geoJsonData)
initializeMap();
});
const getTrackMeta = async (id: number) => {
const headers: Headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Accept', 'application/json');
const request: RequestInfo = new Request(`http://localhost:5000/track/meta?id=${id}`, {
method: "GET",
headers: headers
});
var response = await fetch(request);
// make sure the request was successful
if (response.ok) {
var jsonBody = await response.json();
tracks.value.push({
id: jsonBody["id"],
driver: jsonBody["driver"]["name"],
vehicle: {
name: jsonBody["vehicle"]["name"],
licenseplate: jsonBody["vehicle"]["licenseplate"] ?? "N/A"
},
distance: jsonBody["distance"]
});
}
};
// Beobachte geoJsonData und füge sie der Karte hinzu, wenn sie sich ändert
watch(
() => props.geoJsonData,
(newData) => {
if (newData && newData.features && newData.features.length > 0) {
console.log("Neue GeoJSON-Daten erhalten:", newData);
// Stelle sicher, dass die Karte initialisiert wurde
if (mapInstance.value) {
// Leere nur den GeoJSON Layer, nicht die Tiles
clearGeoJsonLayer();
// Iteriere über alle Tracks und lade deren Meta-Daten
tracks.value = []; // Leere das Array der Tracks, da wir die Daten neu laden
newData.features.forEach((feature: any) => {
getTrackMeta(feature.properties.track_id); // Track-Meta-Daten abrufen
geoJsonLayer.value = L.geoJSON(feature).addTo(mapInstance.value);
});
// Passende Zoom-Bereich für alle Tracks
const bounds = geoJsonLayer.value.getBounds();
mapInstance.value.fitBounds(bounds);
} else {
console.error("Die Karte oder GeoJSON-Daten sind nicht verfügbar.");
}
} else {
console.error("Keine gültigen GeoJSON-Daten gefunden.");
}
},
{ immediate: true }
);
return {
mapDiv,
tracks
};
},
});
</script>
<template>
<!-- Das ref-Attribut gibt den Container für die Karte an -->
<div ref="mapDiv" style="width: 70vw; height: 75vh; overflow: hidden;"></div>
</template>
<style scoped>
#mapDiv {
width: 100vw;
height: 70vh;
margin: 0;
padding: 0;
overflow: hidden;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>

View File

@ -1,79 +1,189 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, SetupContext, ref, onMounted, Ref } from "vue"; import { defineComponent, SetupContext, ref, onMounted, Ref, watch } from "vue";
import * as THREE from "three"; import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
type driver = {
name: string
id: number
}
type Route = { type Route = {
name: string; name: string;
id: number; id: number;
time: Date; time: Date;
driver: {id:number, name:string}
}; };
export default defineComponent({ export default defineComponent({
name: "ClickablePointCloud", emits: ['close', 'response'],
emits: ["close"], name: 'settings',
setup(_, { emit }: SetupContext) { props: {
routes: {
type: Array as () => Route[],
default: () => [], // Standard: leeres Array
required: true,
},
},
setup(props, { emit }: SetupContext) {
console.log(props)
if (!props.routes || props.routes.length <= 0) alert("no points to show");
const canvasRef: Ref<HTMLCanvasElement | null> = ref(null); const canvasRef: Ref<HTMLCanvasElement | null> = ref(null);
const tooltipRef: Ref<HTMLDivElement | null> = ref(null); const tooltipRef: Ref<HTMLDivElement | null> = ref(null);
const tooltipText = ref(""); const tooltipText = ref("");
const router = useRouter(); const router = useRouter();
const legend = ref<Record<string, string>>({});
const driverColors = ref<Record<string, string>>({});
var drivers:driver[] = [];
const points:Route[] = props.routes;
const close = () => { // handles sending webrequests to the backend
emit("close"); const getDrivers = async () => {
};
const headers: Headers = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Accept', 'application/json')
const request: RequestInfo = new Request("http://localhost:5000/driver", {
method: "GET",
headers: headers
})
var response = await fetch(request)
// make sure the request was successfull
if (response.ok) {
var jsonBody = await response.json()
// convert vehicles from json response to processable data
for (let i = 0; i < jsonBody.length; i++) {
let driver = jsonBody[i]
drivers.push({ id: driver["id"], name: driver["name"] })
const color = new THREE.Color(`hsl(${(i * 360) / drivers.length}, 70%, 50%)`);
driverColors.value[driver["id"]] = color.getStyle();
console.log(color.getStyle())
legend.value[driver["id"]] = driver["name"];
}
} else {
console.log(await response.text())
}
}
// assigns a driver to a point based on the points driver id
const assignDriverToPoint = (points:Route[]) => {
return points.reduce((acc, point) => {
const driverId = point.driver.id;
console.log(`driverid: ${driverId}`)
acc[driverId] = acc[driverId] || [];
acc[driverId].push(point);
return acc;
}, {} as Record<number, Route[]>);
}
// runs only when component has fully mounted
onMounted(() => { onMounted(() => {
getDrivers();
if (!canvasRef.value) return; if (!canvasRef.value) return;
// Szene, Kamera und Renderer einrichten console.log(`routes: ${props.routes}`)
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
75, const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value, antialias: true, });
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({
canvas: canvasRef.value,
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
// OrbitControls aktivieren
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
// Raycaster und Maus
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(); const mouse = new THREE.Vector2();
// Beispiel-Daten (Punkte) const groupedPoints = assignDriverToPoint(points)
const points: Route[] = Array.from({ length: 100 }, (_, i) => ({
name: `Route ${i + 1}`,
id: i + 1,
time: new Date(Date.now() - i * 1000 * 60),
}));
// Punktwolke erstellen
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array( const vertices: number[] = [];
points.flatMap((_, i) => [ const colors: number[] = [];
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20, const totalPoints = points.length;
(Math.random() - 0.5) * 20, const offsetXStep = 2; // Wir setzen den Abstand pro Gruppe
])
); let offsetX = 0;
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
// Berechnungen für den Mittelpunkt der Punktwolke
let sumX = 0, sumY = 0, sumZ = 0;
const getColorForDriver = (driverId: number) => {
const color = driverColors.value[driverId];
if (!color) {
console.error(`No color found for driver ID ${driverId}`);
return "#FFFFFF"; // Rückfallfarbe, wenn keine Farbe gefunden wurde
}
return color;
};
// complicated math things
Object.entries(groupedPoints).forEach(([driverIdStr, driverPoints], index) => {
const driverId = parseInt(driverIdStr, 10);
console.log("Driver Colors:", driverColors.value[driverIdStr]);
if (!driverColors.value[driverId]) {
console.error(`Missing color for Driver ID: ${driverId}`);
}
const color = new THREE.Color(driverColors.value[driverId]);
console.log(`Color for Driver ${driverId}:`, driverColors.value[driverId]);
// calculate offset base on driverid
offsetX += offsetXStep;
driverPoints.forEach(() => {
// calculate random position in the area of a driver
const x = offsetX + (Math.random() - 0.5) * 2;
const y = (Math.random() - 0.5) * 6;
const z = (Math.random() - 0.5) * 6;
// calculate center
sumX += x;
sumY += y;
sumZ += z;
vertices.push(x, y, z);
colors.push(color.r, color.g, color.b);
});
});
// Mittelpunkt der Punktwolke berechnen
const centerX = sumX / totalPoints;
const centerY = sumY / totalPoints;
const centerZ = sumZ / totalPoints;
// put cloud centered on the canvase
const offset = new THREE.Vector3(-centerX, -centerY, -centerZ);
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] += offset.x;
vertices[i + 1] += offset.y;
vertices[i + 2] += offset.z;
}
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ const material = new THREE.PointsMaterial({
color: 0x0077ff, vertexColors: true,
size: 0.3, size: 0.3,
}); });
const pointCloud = new THREE.Points(geometry, material); const pointCloud = new THREE.Points(geometry, material);
scene.add(pointCloud); scene.add(pointCloud);
// Kamera und Animation // animate camera
camera.position.set(10, 10, 25); camera.position.set(10, 10, 25);
controls.update(); controls.update();
@ -84,16 +194,14 @@ export default defineComponent({
} }
animate(); animate();
// Mausbewegung (für Tooltip) // handles mouse actions
const onMouseMove = (event: MouseEvent) => { const onMouseMove = (event: MouseEvent) => {
if (!canvasRef.value || !tooltipRef.value) return; if (!canvasRef.value || !tooltipRef.value) return;
// Mausposition berechnen (normalisiert)
const rect = canvasRef.value.getBoundingClientRect(); const rect = canvasRef.value.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Raycasting
raycaster.setFromCamera(mouse, camera); raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(pointCloud); const intersects = raycaster.intersectObject(pointCloud);
@ -102,31 +210,31 @@ export default defineComponent({
const point = points[intersectIndex]; const point = points[intersectIndex];
tooltipRef.value.style.display = "block"; tooltipRef.value.style.display = "block";
tooltipRef.value.style.left = `${event.clientX}px`;
tooltipRef.value.style.top = `${event.clientY - 20}px`; // 3D-Koordinaten des Punktes holen
tooltipText.value = `${point.name} (${point.time.toLocaleString()})`; const positionArray = pointCloud.geometry.attributes.position.array;
const x = positionArray[intersectIndex * 3];
const y = positionArray[intersectIndex * 3 + 1];
const z = positionArray[intersectIndex * 3 + 2];
// 3D-Koordinaten in 2D Bildschirmkoordinaten umrechnen
const screenPosition = new THREE.Vector3(x, y, z);
screenPosition.project(camera);
// Umrechnung von NDC (Normalized Device Coordinates) zu Pixeln
const tooltipX = (screenPosition.x * 0.5 + 0.5) * window.innerWidth;
const tooltipY = (screenPosition.y * -0.5 + 0.5) * window.innerHeight;
tooltipRef.value.style.left = `${tooltipX + 10}px`; // 10px Abstand zum Punkt
tooltipRef.value.style.top = `${tooltipY + 10}px`; // 10px Abstand zum Punkt
tooltipText.value = `${point.driver.name}: ${point.name} (${point.time.toLocaleString()})`;
} else { } else {
tooltipRef.value.style.display = "none"; tooltipRef.value.style.display = "none";
} }
}; }
// Punkt klicken
const onMouseClick = (event: MouseEvent) => {
if (!canvasRef.value) return;
// Raycasting
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(pointCloud);
if (intersects.length > 0) {
const intersectIndex = Math.floor(intersects[0].index || 0);
const point = points[intersectIndex];
router.push(`/routeViewer/${point.id}`);
}
};
canvasRef.value.addEventListener("mousemove", onMouseMove); canvasRef.value.addEventListener("mousemove", onMouseMove);
canvasRef.value.addEventListener("click", onMouseClick);
// Fenstergröße anpassen // Fenstergröße anpassen
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
@ -134,44 +242,86 @@ export default defineComponent({
camera.aspect = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
}); });
}); })
return { return {
close,
canvasRef, canvasRef,
tooltipRef, tooltipRef,
tooltipText, tooltipText,
legend,
driverColors,
}; };
}, },
}); });
</script> </script>
<template> <template>
<div class="pointcloud-container relative"> <div class="pointcloud-container relative">
<canvas ref="canvasRef"></canvas> <canvas ref="canvasRef"></canvas>
<div ref="tooltipRef" class="tooltip hidden fixed z-50 px-2 py-1 bg-base-200 text-base-content rounded"> <div ref="tooltipRef" class="tooltip hidden fixed z-50 px-2 py-1 bg-base-200 text-base-content rounded">
{{ tooltipText }} {{ tooltipText }}
</div> </div>
<!-- Legende -->
<div class="legend absolute top-10 left-10 bg-base-200 p-4 rounded shadow-lg z-50">
<h3 class="font-bold mb-2">Fahrer Legende</h3>
<ul>
<li v-for="(driverName, driverId) in legend" :key="driverId" class="flex items-center mb-1">
<div class="w-4 h-4" :style="{ backgroundColor: driverColors[driverId] }"></div>
<span class="ml-2">{{ driverName }}</span>
</li>
</ul>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.pointcloud-container { .pointcloud-container {
width: 100%; width: 100%;
height: calc(90vh); height: calc(90vh);
overflow: hidden; overflow: hidden;
} }
canvas { canvas {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.btn.close { .tooltip {
position: absolute; display: none;
top: 10px; position: absolute;
right: 10px; z-index: 50;
z-index: 10; padding: 2px 8px;
} background-color: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
pointer-events: none;
}
.legend {
width: auto;
max-width: 250px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 8px;
font-size: 14px;
}
.legend ul {
padding: 0;
list-style-type: none;
}
.legend li {
display: flex;
align-items: center;
}
.legend .color-box {
width: 15px;
height: 15px;
margin-right: 10px;
}
</style> </style>

View File

@ -5,16 +5,18 @@ import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css' import '@vuepic/vue-datepicker/dist/main.css'
import Map from '../components/map.vue'; import Map from '../components/map.vue';
import FileUpload from '../components/fileUpload.vue'; import FileUpload from '../components/fileUpload.vue';
import MapMultiple from '../components/mapMultiple.vue';
type DriverType = { type DriverType = {
id: number id: number
name: string name: string
} }
type track = { type Track = {
id: number id: number
name: string name: string
driver: DriverType driver: DriverType
time: Date
} }
type GeoJSONFeature = { type GeoJSONFeature = {
@ -42,10 +44,13 @@ export default defineComponent({
const showCloud: Ref<boolean> = ref(false); const showCloud: Ref<boolean> = ref(false);
const search: Ref<boolean> = ref(false); const search: Ref<boolean> = ref(false);
const showUpload: Ref<boolean> = ref(false); const showUpload: Ref<boolean> = ref(false);
const tracks: Ref<track[]> = ref([]) const tracks: Ref<Track[]> = ref([])
const searchedTracks: Ref<Track[]> = ref([])
const startSearchDate = ref(); const startSearchDate = ref();
const endSearchDate = ref(); const endSearchDate = ref();
const mapData: Ref<GeoJSON | null> = ref(null); const mapData: Ref<GeoJSON | null> = ref(null);
const renderSearchOnMap: Ref<Boolean> = ref(false);
const multipleTracks:Ref<Boolean> = ref(false);
const loadTrack = async (id: number) => { const loadTrack = async (id: number) => {
showMap.value = true; showMap.value = true;
@ -95,7 +100,7 @@ export default defineComponent({
// convert vehicles from json response to processable data // convert vehicles from json response to processable data
for (let i = 0; i < jsonBody.length; i++) { for (let i = 0; i < jsonBody.length; i++) {
tracks.value.push({ id: jsonBody[i]["id"], name: jsonBody[i]["name"], driver: { name: "", id: 0 } }) tracks.value.push({ id: jsonBody[i]["id"], name: jsonBody[i]["name"], driver: { name: "", id: 0 }, time: new Date(Date.now()) })
} }
} else { } else {
console.log(await response.text()) console.log(await response.text())
@ -108,34 +113,70 @@ export default defineComponent({
headers.set('Accept', 'application/json') headers.set('Accept', 'application/json')
showMap.value = false; showMap.value = false;
showCloud.value = true;
search.value = false; search.value = false;
showUpload.value = false; showUpload.value = false;
const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value, { if (startSearchDate.value == null || endSearchDate.value == null) {
method: "GET", alert("please give all required infos")
headers: headers showCloud.value = false;
}) search.value = true;
return;
}
var response = await fetch(request) if (renderSearchOnMap.value) {
// make sure the request was successfull const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value + "&asMap=true", {
if (response.ok) { method: "GET",
var jsonBody = await response.json() headers: headers
console.log(jsonBody) })
// convert vehicles from json response to processable data try {
for (let i = 0; i < jsonBody.length; i++) { var response = await fetch(request);
tracks.value.push({
id: jsonBody[i]["id"], if (response.ok) {
name: jsonBody[i]["name"], // Wenn die Antwort OK ist, die Daten verarbeiten
driver: { mapData.value = await response.json();
name: jsonBody[i]["driver"]["name"], console.log("GeoJSON-Daten erfolgreich geladen", mapData.value);
id: jsonBody[i]["driver"]["id"] showMap.value = true;
} } else {
}) console.log(await response.text());
}
} catch (error) {
console.error("Fehler beim Laden der Track-Daten:", error);
} }
showMap.value = true;
multipleTracks.value = true;
} else { } else {
console.log(await response.text()) const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value, {
method: "GET",
headers: headers
})
var response = await fetch(request)
// make sure the request was successfull
if (response.ok) {
var jsonBody = await response.json()
console.log(jsonBody)
// convert vehicles from json response to processable data
for (let i = 0; i < jsonBody.length; i++) {
let track = jsonBody[i]
console.log(`track: ${jsonBody[i]["name"]}`)
searchedTracks.value = [];
searchedTracks.value.push({
id: track["id"],
name: track["name"],
driver: {
name: track["driver"]["name"],
id: track["driver"]["id"]
},
time: track["time"]
})
}
showCloud.value = true;
} else {
console.log(await response.text())
}
} }
} }
@ -148,11 +189,14 @@ export default defineComponent({
tracks, tracks,
loadTrack, loadTrack,
getTracks, getTracks,
searchedTracks,
searchTracks, searchTracks,
startSearchDate, startSearchDate,
endSearchDate, endSearchDate,
renderSearchOnMap,
mapData, mapData,
showUpload showUpload,
multipleTracks
}; };
}, },
}); });
@ -176,7 +220,7 @@ export default defineComponent({
</ul> </ul>
</div> </div>
<div style="width: 70%; margin-left: 5%;"> <div style="width: 70%; margin-left: 5%;">
<PointCloud style="border-radius: 10px;" v-if="showCloud && !search"></PointCloud> <PointCloud :routes="searchedTracks" style="border-radius: 10px;" v-if="showCloud && !search"></PointCloud>
</div> </div>
<div v-if="!showMap && !showCloud && search" style="margin-left: 20%; display:flex;"> <div v-if="!showMap && !showCloud && search" style="margin-left: 20%; display:flex;">
<div> <div>
@ -187,18 +231,26 @@ export default defineComponent({
end time end time
<input class="datepicker" type="date" id="birthday" name="birthday" v-model="endSearchDate"> <input class="datepicker" type="date" id="birthday" name="birthday" v-model="endSearchDate">
</div> </div>
<button v-on:click="searchTracks" class="btn btn-success" style="margin-left: 5%; height:60px; width:120px;">Search</button> <div style="margin-left: 5%;">
show on map:
<input type="checkbox" class="toggle" v-model="renderSearchOnMap" />
</div>
<button v-on:click="searchTracks" class="btn btn-success"
style="margin-left: 5%; height:60px; width:120px;">Search</button>
</div> </div>
<div v-if="showMap && !search && !showCloud"> <div v-if="!multipleTracks && showMap && !search && !showCloud">
<Map :geoJsonData="mapData" <Map :geoJsonData="mapData" style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></Map>
style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></Map> </div>
<div v-if="multipleTracks && showMap && !search && !showCloud">
<MapMultiple :geoJsonData="mapData" style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></MapMultiple>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
input.datePicker { input.datePicker {
padding: 5px; padding: 5px;
border-radius: 5px;; border-radius: 5px;
} ;
}
</style> </style>

View File

@ -4,6 +4,7 @@ import {defineComponent, Ref, ref, SetupContext} from 'vue';
type vehicle = { type vehicle = {
id:number id:number
name:string name:string
licenseplate: string
} }
export default defineComponent({ export default defineComponent({
@ -11,7 +12,8 @@ export default defineComponent({
setup(_, { emit }: SetupContext) { setup(_, { emit }: SetupContext) {
const vehicleName:Ref<string> = ref("") const vehicleName:Ref<string> = ref("")
const vehicleList:Ref<vehicle[]> = ref([{id:1,name:"Bike"}]) const licensePlate:Ref<string> = ref("")
const vehicleList:Ref<vehicle[]> = ref([])
// handles getting all existing drivers // handles getting all existing drivers
const getVehicles = async () => { const getVehicles = async () => {
@ -31,7 +33,13 @@ export default defineComponent({
// convert vehicles from json response to processable data // convert vehicles from json response to processable data
for(let i = 0; i < jsonBody.length; i++) { for(let i = 0; i < jsonBody.length; i++) {
vehicleList.value.push({id: jsonBody[i]["id"], name: jsonBody[i]["name"]}) let plate = "N/A"
let vehicle = jsonBody[i]
if (vehicle["licensePlate"] != undefined) {
plate = vehicle["licensePlate"];
}
vehicleList.value.push({id: vehicle["id"], name: vehicle["name"], licenseplate: plate})
} }
} else { } else {
console.log(await response.text()) console.log(await response.text())
@ -45,7 +53,7 @@ export default defineComponent({
headers.set('Content-Type', 'application/json') headers.set('Content-Type', 'application/json')
headers.set('Accept', 'application/json') headers.set('Accept', 'application/json')
const requestBody = JSON.stringify({ name: vehicleName.value }); const requestBody = JSON.stringify({ name: vehicleName.value, licensePlate: licensePlate.value });
const request: RequestInfo = new Request("http://localhost:5000/vehicle", { const request: RequestInfo = new Request("http://localhost:5000/vehicle", {
method:"POST", method:"POST",
@ -57,7 +65,7 @@ export default defineComponent({
// make sure the request was successfull // make sure the request was successfull
if (response.ok){ if (response.ok){
var jsonBody = await response.json() var jsonBody = await response.json()
vehicleList.value.push({id: jsonBody["body"], name: vehicleName.value}) vehicleList.value.push({id: jsonBody["body"], name: vehicleName.value, licenseplate: "N/A"})
} else { } else {
console.log(await response.text()) console.log(await response.text())
} }
@ -67,6 +75,7 @@ export default defineComponent({
return { return {
createVehicle, createVehicle,
vehicleName, vehicleName,
licensePlate,
vehicleList vehicleList
}; };
}, },
@ -89,12 +98,14 @@ export default defineComponent({
<tr> <tr>
<td></td> <td></td>
<td><input type="text" placeholder="Vehicle Name" class="input input-bordered w-full max-w-xs" v-model="vehicleName"/></td> <td><input type="text" placeholder="Vehicle Name" class="input input-bordered w-full max-w-xs" v-model="vehicleName"/></td>
<td><input type="text" placeholder="License Plate" class="input input-bordered w-full max-w-xs" v-model="licensePlate"/></td>
<td><a class="btn btn-success" v-on:click="createVehicle">Create Vehicle</a></td> <td><a class="btn btn-success" v-on:click="createVehicle">Create Vehicle</a></td>
</tr> </tr>
<tr v-for="vehicle in vehicleList"> <tr v-for="vehicle in vehicleList">
<th>{{ vehicle.id }}</th> <th>{{ vehicle.id }}</th>
<td>{{ vehicle.name }}</td> <td>{{ vehicle.name }}</td>
<td><a class="btn btn-warning">Open Editor</a></td> <td>{{ vehicle.licenseplate }}</td>
<td></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>