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
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 47s
This commit is contained in:
parent
8a59bbbea7
commit
df1795c0bd
51
app.py
51
app.py
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
@ -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
146593
uploads/AA_WITAA333_001.gpx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
|
141
web/src/components/mapMultiple.vue
Normal file
141
web/src/components/mapMultiple.vue
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user