This commit is contained in:
steev 2025-01-15 21:18:14 +01:00
commit 9b46ac186d
12 changed files with 147191 additions and 161 deletions

47
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:
tracks = gpxHandler.getTracks()
if len(tracks) > 0:
# gets all tracks as list # gets all tracks as list
return gpxHandler.getTracks() 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,6 +139,7 @@ def handleDriverRoute():
try: try:
drivers = driverHandler.getDrivers() drivers = driverHandler.getDrivers()
if len(drivers) > 0:
return drivers, 200 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}")
@ -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

@ -23,8 +23,10 @@ export default defineComponent({
const form = ref<HTMLFormElement>(); const form = ref<HTMLFormElement>();
const drivers: Ref<driver[]> = ref([]) const drivers: Ref<driver[]> = ref([])
const vehicles: Ref<vehicle[]> = ref([]) const vehicles: Ref<vehicle[]> = ref([])
const selectedDriver: Ref<number> = ref(0); const selectedDriverID: Ref<number> = ref(0);
const selectedVehicle: Ref<number> = ref(0); const selectedVehicleID: Ref<number> = ref(0);
const selectedDriverName: Ref<String> = ref("N/A");
const selectedVehicleName: Ref<String> = ref("N/A");
props.drivers.forEach((d: driver) => { props.drivers.forEach((d: driver) => {
drivers.value.push({ id: d.id, name: d.name }) drivers.value.push({ id: d.id, name: d.name })
@ -108,8 +110,8 @@ export default defineComponent({
const formData = new FormData(); const formData = new FormData();
formData.append("file", file.value); formData.append("file", file.value);
formData.append("driverID", selectedDriver.value.toString()); formData.append("driverID", selectedDriverID.value.toString());
formData.append("vehicleID", selectedVehicle.value.toString()); formData.append("vehicleID", selectedVehicleID.value.toString());
try { try {
@ -146,8 +148,10 @@ export default defineComponent({
return { return {
close, close,
fileInputChange, fileInputChange,
selectedDriver, selectedDriverID,
selectedVehicle, selectedVehicleID,
selectedDriverName,
selectedVehicleName,
save, save,
drivers, drivers,
vehicles, vehicles,
@ -177,15 +181,15 @@ export default defineComponent({
class="file-input file-input-bordered w-full max-w-xs" /> class="file-input file-input-bordered w-full max-w-xs" />
</label> </label>
<div class="dropdown dropdown-bottom"> <div class="dropdown dropdown-bottom">
select Driver: <div tabindex="0" role="button" class="btn m-1">Click</div> select Driver: <div tabindex="0" role="button" class="btn m-1"> {{ selectedDriverName }}</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"> <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li v-for="driver in drivers"><a v-on:click="selectedDriver = driver.id">{{ driver.name }}</a></li> <li v-for="driver in drivers"><a v-on:click="selectedDriverID = driver.id; selectedDriverName=driver.name">{{ driver.name }}</a></li>
</ul> </ul>
</div> </div>
<div class="dropdown dropdown-bottom"> <div class="dropdown dropdown-bottom">
select Vehicle: <div tabindex="0" role="button" class="btn m-1">Click</div> select Vehicle: <div tabindex="0" role="button" class="btn m-1"> {{ selectedVehicleName }}</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"> <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li v-for="vehicle in vehicles"><a v-on:click="selectedVehicle = vehicle.id">{{ vehicle.name }}</a></li> <li v-for="vehicle in vehicles"><a v-on:click="selectedVehicleID = vehicle.id; selectedVehicleName=vehicle.name">{{ vehicle.name }}</a></li>
</ul> </ul>
</div> </div>
<button class="btn btn-success" v-on:click="save">Upload</button> <button class="btn btn-success" v-on:click="save">Upload</button>

View File

@ -1,19 +1,41 @@
<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
name: string
driver: string
vehicle: {
name: string
licenseplate: string
}
distance: number
}
export default defineComponent({ export default defineComponent({
name: 'Map', name: 'Map',
props: { props: {
track: {
type: Number,
default: 0,
require: true,
},
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: 0, name: "N/A", driver: "N/A", vehicle: { name: "N/A", licenseplate: "N/A" }, distance: 0.0 })
const initializeMap = () => { const initializeMap = () => {
if (mapDiv.value) { if (mapDiv.value) {
@ -31,13 +53,63 @@ export default defineComponent({
initializeMap(); initializeMap();
}); });
const clearMap = () => {
if (mapInstance.value) {
mapInstance.value.eachLayer((layer: any) => {
// Nur Layer entfernen, die keine Basemap sind
if (!(layer instanceof L.TileLayer)) {
mapInstance.value.removeLayer(layer);
}
});
}
};
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()
console.log(jsonBody)
track.value = {
id: jsonBody["id"],
name: jsonBody["name"],
driver: jsonBody["driver"]["name"],
vehicle: {
name: jsonBody["vehicle"]["name"],
licenseplate: jsonBody["vehicle"]["licenseplate"] ?? "N/A"
},
distance: jsonBody["distance"]
}
console.log(track.value)
}
}
// 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) {
console.log("Neue GeoJSON-Daten erhalten:", newData); console.log("Neue GeoJSON-Daten erhalten:", newData);
if (mapInstance.value) { if (mapInstance.value) {
const geoJsonLayer = L.geoJSON(newData).addTo(mapInstance.value); // Karte bereinigen
clearMap();
// Neue GeoJSON-Daten hinzufügen
var geoJsonLayer = L.geoJSON(newData).addTo(mapInstance.value);
// Metadaten des Tracks abrufen (z. B. für den ersten Track, falls ID bekannt)
getTrackMeta(props.track);
// Karte an die neuen GeoJSON-Daten anpassen
const bounds = geoJsonLayer.getBounds(); const bounds = geoJsonLayer.getBounds();
mapInstance.value.fitBounds(bounds); mapInstance.value.fitBounds(bounds);
} else { } else {
@ -48,6 +120,7 @@ export default defineComponent({
return { return {
mapDiv, mapDiv,
track
}; };
}, },
}); });
@ -64,16 +137,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.name }}</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>
@ -89,7 +164,8 @@ export default defineComponent({
overflow: hidden; overflow: hidden;
} }
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;

View File

@ -0,0 +1,122 @@
<script lang="ts">
import { defineComponent, onMounted, PropType, ref, watch } from "vue";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
export default defineComponent({
name: "MapMultiple",
props: {
tracks: {
type: Array as PropType<number[]>, // Array von IDs
default: () => [],
required: true,
},
},
setup(props) {
const mapDiv = ref<HTMLElement | null>(null); // Ref für die Karten-Div
const mapInstance = ref<L.Map | null>(null); // Ref für die Leaflet-Karte
const geoJsonLayers = ref<L.LayerGroup | null>(null); // Ref für GeoJSON-Layer
// Initialisiert die Leaflet-Karte
const initializeMap = () => {
if (!mapDiv.value) {
console.error("mapDiv ist nicht verfügbar.");
return;
}
console.log("Initialisiere Karte...");
mapInstance.value = L.map(mapDiv.value, {
center: [51.4819, 7.2162], // Standardzentrum
zoom: 13,
});
// OpenStreetMap-Tiles hinzufügen
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {}).addTo(
mapInstance.value
);
// Initialisiere einen LayerGroup-Container für GeoJSON-Daten
geoJsonLayers.value = L.layerGroup().addTo(mapInstance.value);
console.log("Karte erfolgreich initialisiert.");
};
// Lädt GeoJSON-Daten für eine bestimmte Track-ID
const loadTrack = async (id: number) => {
console.log(`Lade Daten für Track ${id}...`);
try {
const response = await fetch(`http://localhost:5000/track?id=${id}`);
if (!response.ok) {
console.error(`Fehler beim Laden von Track ${id}:`, response.statusText);
return null;
}
const geoJson = await response.json();
console.log(`GeoJSON für Track ${id} erhalten:`, geoJson);
return geoJson;
} catch (error) {
console.error(`Fehler beim Laden von Track ${id}:`, error);
return null;
}
};
// Fügt GeoJSON-Daten zur Karte hinzu
const addGeoJsonToMap = (geoJson: any) => {
if (!geoJsonLayers.value) {
console.error("GeoJSON-Layer ist nicht initialisiert.");
return;
}
const layer = L.geoJSON(geoJson);
geoJsonLayers.value.addLayer(layer);
// Fit the map bounds to the newly added layer
if (mapInstance.value) {
mapInstance.value.fitBounds(layer.getBounds());
}
};
watch(
() => props.tracks,
async (newTracks) => {
console.log("Tracks geändert:", newTracks);
if (!mapInstance.value) {
console.error("Karte ist noch nicht initialisiert.");
return;
}
// Entferne alle vorhandenen GeoJSON-Layer
if (geoJsonLayers.value) {
geoJsonLayers.value.clearLayers();
}
for (const trackId of newTracks) {
const geoJson = await loadTrack(trackId);
if (geoJson) {
await addGeoJsonToMap(geoJson);
}
}
},
{ immediate: true }
);
onMounted(() => {
console.log("Komponente gemountet.");
initializeMap();
});
return {
mapDiv,
};
},
});
</script>
<template>
<div ref="mapDiv" id="mapDiv"></div>
</template>
<style scoped>
#mapDiv {
width: 100%;
height: 75vh;
}
</style>

View File

@ -1,20 +1,36 @@
<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 }; 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("");
@ -29,27 +45,19 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
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();
const groupedPoints = assignDriverToPoint(points)
// Beispiel-Daten (Fahrer mit ID und Punkten) // Beispiel-Daten (Fahrer mit ID und Punkten)
const drivers = [ const drivers = [
{ id: 1, name: "Alice" }, { id: 1, name: "Alice" },
@ -134,6 +142,7 @@ export default defineComponent({
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ const material = new THREE.PointsMaterial({
vertexColors: true,
vertexColors: true, // Farben pro Punkt verwenden vertexColors: true, // Farben pro Punkt verwenden
size: 0.3, size: 0.3,
}); });
@ -141,7 +150,7 @@ export default defineComponent({
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();
@ -152,7 +161,7 @@ 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;
@ -187,9 +196,29 @@ export default defineComponent({
tooltipRef.value.style.top = `${tooltipY + 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()})`; tooltipText.value = `${point.driver.name}: ${point.name} (${point.time.toLocaleString()})`;
// 3D-Koordinaten des Punktes holen
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";
} }
}
}; };
canvasRef.value.addEventListener("mousemove", onMouseMove); canvasRef.value.addEventListener("mousemove", onMouseMove);
@ -200,20 +229,22 @@ 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, legend,
driverColors, driverColors,
legend,
driverColors,
}; };
}, },
}); });
</script> </script>
<template> <template>
<div class="pointcloud-container relative"> <div class="pointcloud-container relative">
<canvas ref="canvasRef"></canvas> <canvas ref="canvasRef"></canvas>
@ -230,6 +261,16 @@ export default defineComponent({
</li> </li>
</ul> </ul>
</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>
@ -277,6 +318,42 @@ canvas {
align-items: center; align-items: center;
} }
.legend .color-box {
width: 15px;
height: 15px;
margin-right: 10px;
}
.tooltip {
display: none;
position: absolute;
z-index: 50;
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 { .legend .color-box {
width: 15px; width: 15px;
height: 15px; height: 15px;

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 = {
@ -36,16 +38,21 @@ type GeoJSON = {
export default defineComponent({ export default defineComponent({
name: 'map', name: 'map',
components: { PointCloud, VueDatePicker, Map, FileUpload }, components: { PointCloud, VueDatePicker, Map, MapMultiple, FileUpload },
setup(_, { emit }: SetupContext) { setup(_, { emit }: SetupContext) {
const showMap: Ref<boolean> = ref(false); const showMap: Ref<boolean> = ref(false);
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 trackid: Ref<number> = ref(0)
const trackids: Ref<number[]> = ref([])
const loadTrack = async (id: number) => { const loadTrack = async (id: number) => {
showMap.value = true; showMap.value = true;
@ -53,6 +60,8 @@ export default defineComponent({
search.value = false; search.value = false;
showUpload.value = false; showUpload.value = false;
trackid.value = id
const headers: Headers = new Headers(); const headers: Headers = new Headers();
headers.set('Content-Type', 'application/json'); headers.set('Content-Type', 'application/json');
headers.set('Accept', 'application/json'); headers.set('Accept', 'application/json');
@ -95,7 +104,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,45 +117,76 @@ 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;
// Formatierung der Datumsangaben if (startSearchDate.value == null || endSearchDate.value == null) {
const startDate = startSearchDate.value ? new Date(startSearchDate.value).toISOString().split('T')[0] : null; alert("please give all required infos")
const endDate = endSearchDate.value ? new Date(endSearchDate.value).toISOString().split('T')[0] : null; showCloud.value = false;
search.value = true;
return;
}
// Baue den Request-URL mit den formatierten Datumsangaben if (renderSearchOnMap.value) {
const url = new URL("http://localhost:5000/track"); const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value, {
if (startDate) url.searchParams.append("start", startDate);
if (endDate) url.searchParams.append("end", endDate);
const request: RequestInfo = new Request(url.toString(), {
method: "GET", method: "GET",
headers: headers, headers: headers
}); })
try {
var response = await fetch(request);
if (response.ok) {
// Wenn die Antwort OK ist, die Daten verarbeiten
var resp = await response.json()
for (let i = 0; i < resp.length; i++) {
trackids.value.push(resp[i]["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 {
const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value, {
method: "GET",
headers: headers
})
var response = await fetch(request) var response = await fetch(request)
// 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()
console.log(jsonBody) console.log(jsonBody)
searchedTracks.value = [];
// 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({ let track = jsonBody[i]
id: jsonBody[i]["id"], console.log(`track: ${jsonBody[i]["name"]}`)
name: jsonBody[i]["name"], searchedTracks.value.push({
id: track["id"],
name: track["name"],
driver: { driver: {
name: jsonBody[i]["driver"]["name"], name: track["driver"]["name"],
id: jsonBody[i]["driver"]["id"] id: track["driver"]["id"]
} },
time: track["time"]
}) })
} }
showCloud.value = true;
} else { } else {
console.log(await response.text()) console.log(await response.text())
} }
} }
}
getTracks() getTracks()
@ -157,11 +197,16 @@ export default defineComponent({
tracks, tracks,
loadTrack, loadTrack,
getTracks, getTracks,
searchedTracks,
searchTracks, searchTracks,
startSearchDate, startSearchDate,
endSearchDate, endSearchDate,
renderSearchOnMap,
mapData, mapData,
showUpload showUpload,
multipleTracks,
trackid,
trackids
}; };
}, },
}); });
@ -185,7 +230,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>
@ -196,11 +241,18 @@ 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> </div>
<div v-if="showMap && !search && !showCloud"> <button v-on:click="searchTracks" class="btn btn-success"
<Map :geoJsonData="mapData" style="margin-left: 5%; height:60px; width:120px;">Search</button>
style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></Map> </div>
<div v-if="!multipleTracks && showMap && !search && !showCloud">
<Map :track="trackid" :geoJsonData="mapData" style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></Map>
</div>
<div v-if="multipleTracks && showMap && !search && !showCloud">
<MapMultiple :tracks="trackids" style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></MapMultiple>
</div> </div>
</div> </div>
</template> </template>
@ -208,6 +260,7 @@ export default defineComponent({
<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,6 +12,7 @@ export default defineComponent({
setup(_, { emit }: SetupContext) { setup(_, { emit }: SetupContext) {
const vehicleName:Ref<string> = ref("") const vehicleName:Ref<string> = ref("")
const licensePlate:Ref<string> = ref("")
const vehicleList:Ref<vehicle[]> = ref([]) const vehicleList:Ref<vehicle[]> = ref([])
// handles getting all existing drivers // handles getting all existing drivers
@ -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>