Merge branch 'main' of https://git.slpnetwork.de/Steev/GeoTracking
This commit is contained in:
commit
9b46ac186d
47
app.py
47
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:
|
||||
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
|
||||
start = request.args["start"]
|
||||
end = request.args["end"]
|
||||
@ -78,12 +90,36 @@ def getTrack():
|
||||
return f"error {e}", 500
|
||||
else:
|
||||
try:
|
||||
tracks = gpxHandler.getTracks()
|
||||
if len(tracks) > 0:
|
||||
# gets all tracks as list
|
||||
return gpxHandler.getTracks()
|
||||
return tracks, 200
|
||||
else:
|
||||
return [], 200
|
||||
except Exception as e:
|
||||
app.logger.debug(f"fetching all tracks failed with error {e}")
|
||||
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'])
|
||||
@cross_origin()
|
||||
@ -103,6 +139,7 @@ def handleDriverRoute():
|
||||
|
||||
try:
|
||||
drivers = driverHandler.getDrivers()
|
||||
if len(drivers) > 0:
|
||||
return drivers, 200
|
||||
except Exception as e:
|
||||
app.logger.debug(f"getting drivers failed with error {e}")
|
||||
@ -163,11 +200,17 @@ def handleVehicleRoute():
|
||||
if "name" not in data:
|
||||
return "missing name", 400
|
||||
|
||||
licenseplate = ""
|
||||
|
||||
if "licensePlate" not in data:
|
||||
licenseplate = "N/A"
|
||||
else:
|
||||
licenseplate = data["licensePlate"]
|
||||
name = data["name"]
|
||||
|
||||
# handle creating vehicle
|
||||
try:
|
||||
vehicle = vehicleHandler.createVehicle(name)
|
||||
vehicle = vehicleHandler.createVehicle(name, licenseplate)
|
||||
return jsonify({"id": vehicle.id, "name": vehicle.name}), 200
|
||||
|
||||
except Exception as e:
|
||||
|
@ -17,10 +17,10 @@ services:
|
||||
POSTGRES_USER: example
|
||||
POSTGRES_PASSWORD: example
|
||||
POSTGRES_DB: geotrack
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:5000"
|
||||
# web:
|
||||
# build: .
|
||||
# ports:
|
||||
# - "8000:5000"
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
|
@ -6,7 +6,6 @@ from sqlalchemy.exc import OperationalError
|
||||
|
||||
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
|
||||
def db_connect():
|
||||
try:
|
||||
@ -30,20 +29,23 @@ def create_table(engine):
|
||||
except Exception as e:
|
||||
print(f"Fehler bei der Tabellenerstellung: {e}")
|
||||
|
||||
# Track-Tabelle
|
||||
class Track(Base):
|
||||
__tablename__ = 'track'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
trackName = Column(String(200), nullable=True)
|
||||
vehicle_id = Column(Integer, ForeignKey('vehicle.id'), nullable=False, default=1)
|
||||
driver_id = Column(Integer, ForeignKey('driver.id'), nullable=False, default=1)
|
||||
vehicle_id = Column(Integer, ForeignKey('vehicle.id'), nullable=False)
|
||||
driver_id = Column(Integer, ForeignKey('driver.id'), nullable=False)
|
||||
date = Column(Date, nullable=True)
|
||||
distance = Column(Float, nullable=False, default=0)
|
||||
speed = Column(Float, nullable=False, default=0)
|
||||
distance = Column(Float, nullable=False, default=0.0)
|
||||
speed = Column(Float, nullable=False, default=0.0)
|
||||
|
||||
driver = relationship("Driver", backref="vehicle_tracks") # 'vehicle_tracks' als backref
|
||||
vehicle = relationship("Vehicle", backref="driver_tracks") # 'driver_tracks' als backref
|
||||
waypoints = relationship('Waypoint', backref='track', lazy=True)
|
||||
# Beziehungen
|
||||
driver = relationship("Driver", back_populates="tracks", foreign_keys=[driver_id])
|
||||
vehicle = relationship("Vehicle", back_populates="tracks", foreign_keys=[vehicle_id])
|
||||
waypoints = relationship('Waypoint', back_populates='track', lazy=True)
|
||||
|
||||
# Waypoint-Tabelle
|
||||
class Waypoint(Base):
|
||||
__tablename__ = 'waypoint'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
@ -54,16 +56,24 @@ class Waypoint(Base):
|
||||
time = Column(DateTime, nullable=True)
|
||||
track_id = Column(Integer, ForeignKey('track.id'), nullable=False)
|
||||
|
||||
# Beziehung zu Track
|
||||
track = relationship("Track", back_populates="waypoints")
|
||||
|
||||
# Driver-Tabelle
|
||||
class Driver(Base):
|
||||
__tablename__ = 'driver'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
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):
|
||||
__tablename__ = 'vehicle'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
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.gpx
|
||||
from sqlalchemy.orm import Session
|
||||
from geojson import Feature, LineString
|
||||
from geojson import Feature, LineString, FeatureCollection
|
||||
from geopy.distance import geodesic
|
||||
|
||||
from errors.NotFoundException import NotFoundError
|
||||
@ -92,6 +92,31 @@ class GPXHandler:
|
||||
feature = Feature(geometry=LineString(coordinates))
|
||||
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
|
||||
def getTracks(self):
|
||||
tracks = self.__dbSession.query(Track).all()
|
||||
@ -100,23 +125,39 @@ class GPXHandler:
|
||||
{
|
||||
"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
|
||||
# } 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,
|
||||
"time": track.date,
|
||||
}
|
||||
for track in tracks # iterates all tracks and appends them to the 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):
|
||||
tracks = self.__dbSession.query(Track).filter(Track.date.between(start, end)).all()
|
||||
@ -134,8 +175,7 @@ class GPXHandler:
|
||||
"name": track.vehicle.name
|
||||
} if track.vehicle else None,
|
||||
"distance": track.distance,
|
||||
"startTime": track.start.isoformat() if track.start else None,
|
||||
"end_time": track.end.isoformat() if track.end else None,
|
||||
"time": track.date,
|
||||
}
|
||||
for track in tracks # iterates all tracks and appends them to the list
|
||||
]
|
||||
|
@ -10,11 +10,11 @@ class VehicleHandler:
|
||||
pass
|
||||
|
||||
# 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:
|
||||
raise ValueError("name is empty")
|
||||
|
||||
vehicle = Vehicle(name=name)
|
||||
vehicle = Vehicle(name=name, licenseplate=licenseplate)
|
||||
|
||||
self.dbSession.add(vehicle)
|
||||
self.dbSession.commit()
|
||||
@ -34,7 +34,8 @@ class VehicleHandler:
|
||||
driverList = [
|
||||
{
|
||||
"id": vehicle.id,
|
||||
"name": vehicle.name
|
||||
"name": vehicle.name,
|
||||
"licensePlate": vehicle.licenseplate
|
||||
}
|
||||
# iterates all drivers and appends them to the list
|
||||
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
@ -23,8 +23,10 @@ export default defineComponent({
|
||||
const form = ref<HTMLFormElement>();
|
||||
const drivers: Ref<driver[]> = ref([])
|
||||
const vehicles: Ref<vehicle[]> = ref([])
|
||||
const selectedDriver: Ref<number> = ref(0);
|
||||
const selectedVehicle: Ref<number> = ref(0);
|
||||
const selectedDriverID: 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) => {
|
||||
drivers.value.push({ id: d.id, name: d.name })
|
||||
@ -108,8 +110,8 @@ export default defineComponent({
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.value);
|
||||
formData.append("driverID", selectedDriver.value.toString());
|
||||
formData.append("vehicleID", selectedVehicle.value.toString());
|
||||
formData.append("driverID", selectedDriverID.value.toString());
|
||||
formData.append("vehicleID", selectedVehicleID.value.toString());
|
||||
|
||||
try {
|
||||
|
||||
@ -146,8 +148,10 @@ export default defineComponent({
|
||||
return {
|
||||
close,
|
||||
fileInputChange,
|
||||
selectedDriver,
|
||||
selectedVehicle,
|
||||
selectedDriverID,
|
||||
selectedVehicleID,
|
||||
selectedDriverName,
|
||||
selectedVehicleName,
|
||||
save,
|
||||
drivers,
|
||||
vehicles,
|
||||
@ -177,15 +181,15 @@ export default defineComponent({
|
||||
class="file-input file-input-bordered w-full max-w-xs" />
|
||||
</label>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<button class="btn btn-success" v-on:click="save">Upload</button>
|
||||
|
@ -1,19 +1,41 @@
|
||||
<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 L from "leaflet";
|
||||
|
||||
type Track = {
|
||||
id: number
|
||||
name: string
|
||||
driver: string
|
||||
vehicle: {
|
||||
name: string
|
||||
licenseplate: string
|
||||
}
|
||||
distance: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Map',
|
||||
props: {
|
||||
track: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
require: true,
|
||||
},
|
||||
geoJsonData: {
|
||||
type: Object as PropType<any>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
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 track: Ref<Track> = ref({ id: 0, name: "N/A", driver: "N/A", vehicle: { name: "N/A", licenseplate: "N/A" }, distance: 0.0 })
|
||||
|
||||
const initializeMap = () => {
|
||||
if (mapDiv.value) {
|
||||
@ -31,13 +53,63 @@ export default defineComponent({
|
||||
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) => {
|
||||
if (newData) {
|
||||
console.log("Neue GeoJSON-Daten erhalten:", newData);
|
||||
|
||||
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();
|
||||
mapInstance.value.fitBounds(bounds);
|
||||
} else {
|
||||
@ -48,6 +120,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
mapDiv,
|
||||
track
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -64,20 +137,22 @@ export default defineComponent({
|
||||
<th>track name</th>
|
||||
<th>driver</th>
|
||||
<th>vehicle</th>
|
||||
<th>distance Color</th>
|
||||
<th>License Plate</th>
|
||||
<th>distance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr class="bg-base-200">
|
||||
<th>1</th>
|
||||
<td>Cy Ganderton</td>
|
||||
<td>Quality Control Specialist</td>
|
||||
<td>Blue</td>
|
||||
<th>{{ track.name }}</th>
|
||||
<td>{{ track.driver }}</td>
|
||||
<td>{{ track.vehicle.name }}</td>
|
||||
<td>{{ track.vehicle.licenseplate }}</td>
|
||||
<td>{{ Math.round((track.distance / 1000) * 100) / 100 }}KM</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -89,7 +164,8 @@ export default defineComponent({
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
122
web/src/components/mapMultiple.vue
Normal file
122
web/src/components/mapMultiple.vue
Normal 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>
|
@ -1,20 +1,36 @@
|
||||
<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 { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
type driver = {
|
||||
name: string
|
||||
id: number
|
||||
}
|
||||
|
||||
type Route = {
|
||||
name: string;
|
||||
id: number;
|
||||
time: Date;
|
||||
driver: { id: number; name: string };
|
||||
driver: {id:number, name:string}
|
||||
};
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: "ClickablePointCloud",
|
||||
emits: ["close"],
|
||||
setup(_, { emit }: SetupContext) {
|
||||
emits: ['close', 'response'],
|
||||
name: 'settings',
|
||||
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 tooltipRef: Ref<HTMLDivElement | null> = ref(null);
|
||||
const tooltipText = ref("");
|
||||
@ -29,27 +45,19 @@ export default defineComponent({
|
||||
onMounted(() => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
// Szene, Kamera und Renderer einrichten
|
||||
console.log(`routes: ${props.routes}`)
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvasRef.value,
|
||||
antialias: true,
|
||||
});
|
||||
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
|
||||
const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value, antialias: true, });
|
||||
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// OrbitControls aktivieren
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
|
||||
// Raycaster und Maus
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
const groupedPoints = assignDriverToPoint(points)
|
||||
|
||||
// Beispiel-Daten (Fahrer mit ID und Punkten)
|
||||
const drivers = [
|
||||
{ id: 1, name: "Alice" },
|
||||
@ -134,6 +142,7 @@ export default defineComponent({
|
||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||
|
||||
const material = new THREE.PointsMaterial({
|
||||
vertexColors: true,
|
||||
vertexColors: true, // Farben pro Punkt verwenden
|
||||
size: 0.3,
|
||||
});
|
||||
@ -141,7 +150,7 @@ export default defineComponent({
|
||||
const pointCloud = new THREE.Points(geometry, material);
|
||||
scene.add(pointCloud);
|
||||
|
||||
// Kamera und Animation
|
||||
// animate camera
|
||||
camera.position.set(10, 10, 25);
|
||||
controls.update();
|
||||
|
||||
@ -152,7 +161,7 @@ export default defineComponent({
|
||||
}
|
||||
animate();
|
||||
|
||||
// Mausbewegung (für Tooltip)
|
||||
// handles mouse actions
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!canvasRef.value || !tooltipRef.value) return;
|
||||
|
||||
@ -187,9 +196,29 @@ export default defineComponent({
|
||||
tooltipRef.value.style.top = `${tooltipY + 10}px`; // 10px Abstand zum Punkt
|
||||
|
||||
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 {
|
||||
tooltipRef.value.style.display = "none";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
canvasRef.value.addEventListener("mousemove", onMouseMove);
|
||||
@ -200,20 +229,22 @@ export default defineComponent({
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
close,
|
||||
canvasRef,
|
||||
tooltipRef,
|
||||
tooltipText,
|
||||
legend,
|
||||
driverColors,
|
||||
legend,
|
||||
driverColors,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="pointcloud-container relative">
|
||||
<canvas ref="canvasRef"></canvas>
|
||||
@ -230,22 +261,68 @@ export default defineComponent({
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pointcloud-container {
|
||||
.pointcloud-container {
|
||||
width: 100%;
|
||||
height: calc(90vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
@ -5,16 +5,18 @@ import VueDatePicker from '@vuepic/vue-datepicker';
|
||||
import '@vuepic/vue-datepicker/dist/main.css'
|
||||
import Map from '../components/map.vue';
|
||||
import FileUpload from '../components/fileUpload.vue';
|
||||
import MapMultiple from '../components/mapMultiple.vue';
|
||||
|
||||
type DriverType = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
type track = {
|
||||
type Track = {
|
||||
id: number
|
||||
name: string
|
||||
driver: DriverType
|
||||
time: Date
|
||||
}
|
||||
|
||||
type GeoJSONFeature = {
|
||||
@ -36,16 +38,21 @@ type GeoJSON = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'map',
|
||||
components: { PointCloud, VueDatePicker, Map, FileUpload },
|
||||
components: { PointCloud, VueDatePicker, Map, MapMultiple, FileUpload },
|
||||
setup(_, { emit }: SetupContext) {
|
||||
const showMap: Ref<boolean> = ref(false);
|
||||
const showCloud: Ref<boolean> = ref(false);
|
||||
const search: 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 endSearchDate = ref();
|
||||
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) => {
|
||||
showMap.value = true;
|
||||
@ -53,6 +60,8 @@ export default defineComponent({
|
||||
search.value = false;
|
||||
showUpload.value = false;
|
||||
|
||||
trackid.value = id
|
||||
|
||||
const headers: Headers = new Headers();
|
||||
headers.set('Content-Type', 'application/json');
|
||||
headers.set('Accept', 'application/json');
|
||||
@ -95,7 +104,7 @@ export default defineComponent({
|
||||
|
||||
// convert vehicles from json response to processable data
|
||||
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 {
|
||||
console.log(await response.text())
|
||||
@ -108,45 +117,76 @@ export default defineComponent({
|
||||
headers.set('Accept', 'application/json')
|
||||
|
||||
showMap.value = false;
|
||||
showCloud.value = true;
|
||||
search.value = false;
|
||||
showUpload.value = false;
|
||||
|
||||
// Formatierung der Datumsangaben
|
||||
const startDate = startSearchDate.value ? new Date(startSearchDate.value).toISOString().split('T')[0] : null;
|
||||
const endDate = endSearchDate.value ? new Date(endSearchDate.value).toISOString().split('T')[0] : null;
|
||||
if (startSearchDate.value == null || endSearchDate.value == null) {
|
||||
alert("please give all required infos")
|
||||
showCloud.value = false;
|
||||
search.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Baue den Request-URL mit den formatierten Datumsangaben
|
||||
const url = new URL("http://localhost:5000/track");
|
||||
if (startDate) url.searchParams.append("start", startDate);
|
||||
if (endDate) url.searchParams.append("end", endDate);
|
||||
|
||||
const request: RequestInfo = new Request(url.toString(), {
|
||||
if (renderSearchOnMap.value) {
|
||||
const request: RequestInfo = new Request("http://localhost:5000/track?start=" + startSearchDate.value + "&end=" + endSearchDate.value, {
|
||||
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)
|
||||
// make sure the request was successfull
|
||||
if (response.ok) {
|
||||
var jsonBody = await response.json()
|
||||
console.log(jsonBody)
|
||||
searchedTracks.value = [];
|
||||
|
||||
// convert vehicles from json response to processable data
|
||||
for (let i = 0; i < jsonBody.length; i++) {
|
||||
tracks.value.push({
|
||||
id: jsonBody[i]["id"],
|
||||
name: jsonBody[i]["name"],
|
||||
let track = jsonBody[i]
|
||||
console.log(`track: ${jsonBody[i]["name"]}`)
|
||||
searchedTracks.value.push({
|
||||
id: track["id"],
|
||||
name: track["name"],
|
||||
driver: {
|
||||
name: jsonBody[i]["driver"]["name"],
|
||||
id: jsonBody[i]["driver"]["id"]
|
||||
}
|
||||
name: track["driver"]["name"],
|
||||
id: track["driver"]["id"]
|
||||
},
|
||||
time: track["time"]
|
||||
})
|
||||
}
|
||||
showCloud.value = true;
|
||||
|
||||
} else {
|
||||
console.log(await response.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTracks()
|
||||
|
||||
@ -157,11 +197,16 @@ export default defineComponent({
|
||||
tracks,
|
||||
loadTrack,
|
||||
getTracks,
|
||||
searchedTracks,
|
||||
searchTracks,
|
||||
startSearchDate,
|
||||
endSearchDate,
|
||||
renderSearchOnMap,
|
||||
mapData,
|
||||
showUpload
|
||||
showUpload,
|
||||
multipleTracks,
|
||||
trackid,
|
||||
trackids
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -185,7 +230,7 @@ export default defineComponent({
|
||||
</ul>
|
||||
</div>
|
||||
<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 v-if="!showMap && !showCloud && search" style="margin-left: 20%; display:flex;">
|
||||
<div>
|
||||
@ -196,18 +241,26 @@ export default defineComponent({
|
||||
end time
|
||||
<input class="datepicker" type="date" id="birthday" name="birthday" v-model="endSearchDate">
|
||||
</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 v-if="showMap && !search && !showCloud">
|
||||
<Map :geoJsonData="mapData"
|
||||
style="width: 68vw; margin-left: 10%; border-radius: 10px; border: 1px solid #95a5a6;"></Map>
|
||||
<button v-on:click="searchTracks" class="btn btn-success"
|
||||
style="margin-left: 5%; height:60px; width:120px;">Search</button>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
input.datePicker {
|
||||
input.datePicker {
|
||||
padding: 5px;
|
||||
border-radius: 5px;;
|
||||
}
|
||||
border-radius: 5px;
|
||||
;
|
||||
}
|
||||
</style>
|
@ -4,6 +4,7 @@ import {defineComponent, Ref, ref, SetupContext} from 'vue';
|
||||
type vehicle = {
|
||||
id:number
|
||||
name:string
|
||||
licenseplate: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
@ -11,6 +12,7 @@ export default defineComponent({
|
||||
setup(_, { emit }: SetupContext) {
|
||||
|
||||
const vehicleName:Ref<string> = ref("")
|
||||
const licensePlate:Ref<string> = ref("")
|
||||
const vehicleList:Ref<vehicle[]> = ref([])
|
||||
|
||||
// handles getting all existing drivers
|
||||
@ -31,7 +33,13 @@ export default defineComponent({
|
||||
|
||||
// convert vehicles from json response to processable data
|
||||
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 {
|
||||
console.log(await response.text())
|
||||
@ -45,7 +53,7 @@ export default defineComponent({
|
||||
headers.set('Content-Type', '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", {
|
||||
method:"POST",
|
||||
@ -57,7 +65,7 @@ export default defineComponent({
|
||||
// make sure the request was successfull
|
||||
if (response.ok){
|
||||
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 {
|
||||
console.log(await response.text())
|
||||
}
|
||||
@ -67,6 +75,7 @@ export default defineComponent({
|
||||
return {
|
||||
createVehicle,
|
||||
vehicleName,
|
||||
licensePlate,
|
||||
vehicleList
|
||||
};
|
||||
},
|
||||
@ -89,12 +98,14 @@ export default defineComponent({
|
||||
<tr>
|
||||
<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="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>
|
||||
</tr>
|
||||
<tr v-for="vehicle in vehicleList">
|
||||
<th>{{ vehicle.id }}</th>
|
||||
<td>{{ vehicle.name }}</td>
|
||||
<td><a class="btn btn-warning">Open Editor</a></td>
|
||||
<td>{{ vehicle.licenseplate }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
Loading…
x
Reference in New Issue
Block a user