Create/Update Place GPX, beta: show GPX on Map

This commit is contained in:
itskovacs 2025-07-19 16:11:17 +02:00
parent b0cbd0efb5
commit 3005fe9caf
7 changed files with 110 additions and 20 deletions

View File

@ -2,7 +2,8 @@
@if (selectedPlace) {
<app-place-box [selectedPlace]="selectedPlace" (deleteEmitter)="deletePlace()" (editEmitter)="editPlace()"
(favoriteEmitter)="favoritePlace()" (visitEmitter)="visitPlace()" (closeEmitter)="closePlaceBox()"></app-place-box>
(favoriteEmitter)="favoritePlace()" (gpxEmitter)="getPlaceGPX()" (visitEmitter)="visitPlace()"
(closeEmitter)="closePlaceBox()"></app-place-box>
}
<div class="absolute z-30 top-2 right-2 p-2 bg-white shadow rounded">

View File

@ -25,7 +25,12 @@ import { FloatLabelModule } from "primeng/floatlabel";
import { BatchCreateModalComponent } from "../../modals/batch-create-modal/batch-create-modal.component";
import { UtilsService } from "../../services/utils.service";
import { Info } from "../../types/info";
import { createMap, placeToMarker, createClusterGroup } from "../../shared/map";
import {
createMap,
placeToMarker,
createClusterGroup,
gpxToPolyline,
} from "../../shared/map";
import { Router } from "@angular/router";
import { SelectModule } from "primeng/select";
import { MultiSelectModule } from "primeng/multiselect";
@ -82,6 +87,7 @@ export class DashboardComponent implements AfterViewInit {
hoveredElements: HTMLElement[] = [];
map: any;
mapDisplayedTrace: L.Polyline[] = [];
settings: Settings | undefined;
currencySigns: { c: string; s: string }[] = [];
doNotDisplayOptions: SelectItemGroup[] = [];
@ -499,6 +505,42 @@ export class DashboardComponent implements AfterViewInit {
});
}
displayGPXOnMap(gpx: string) {
try {
// HINT: For now, delete traces everytime we display a GPX
// TODO: Handle multiple polygons and handle Click events
this.mapDisplayedTrace.forEach((p) => this.map.removeLayer(p));
this.mapDisplayedTrace = [];
const gpxPolyline = gpxToPolyline(gpx).addTo(this.map);
gpxPolyline.on("click", () => {
this.map.removeLayer(gpxPolyline);
});
this.mapDisplayedTrace.push(gpxPolyline);
} catch {
this.utilsService.toast("error", "Error", "Couldn't parse GPX data");
return;
}
}
getPlaceGPX() {
if (!this.selectedPlace) return;
this.apiService.getPlaceGPX(this.selectedPlace.id).subscribe({
next: (p) => {
if (!p.gpx) {
this.utilsService.toast(
"error",
"Error",
"Couldn't retrieve GPX data",
);
return;
}
this.displayGPXOnMap(p.gpx);
},
});
}
toggleSettings() {
this.viewSettings = !this.viewSettings;
if (this.viewSettings && this.settings) {

View File

@ -43,14 +43,25 @@
<label for="price">Price</label>
</p-floatlabel>
<div class="flex justify-center items-center">
<p-checkbox formControlName="allowdog" [binary]="true" inputId="allowdog" />
<label for="allowdog" class="ml-2">Allow 🐶</label>
</div>
<div class="col-span-2 grid grid-cols-2 md:grid-cols-3 gap-4">
<div class="flex justify-center items-center">
<p-checkbox formControlName="allowdog" [binary]="true" inputId="allowdog" />
<label for="allowdog" class="ml-2">Allow 🐶</label>
</div>
<div class="flex justify-center items-center">
<p-checkbox formControlName="visited" [binary]="true" inputId="visited" />
<label for="visited" class="ml-2">Visited</label>
<div class="flex justify-center items-center">
<p-checkbox formControlName="visited" [binary]="true" inputId="visited" />
<label for="visited" class="ml-2">Visited</label>
</div>
<div class="col-span-2 md:col-span-1 flex justify-center items-center">
@if (placeForm.get('gpx')?.value) {
<p-button text icon="pi pi-times" label="GPX" severity="danger" (click)="clearGPX()" />
} @else {
<p-button text icon="pi pi-paperclip" label="GPX" (click)="gpxInput.click()" />
}
<input type="file" accept=".gpx" #gpxInput class="hidden" (change)="onGPXSelected($event)" />
</div>
</div>
<div class="grid col-span-full md:grid-cols-4">
@ -61,7 +72,7 @@
<div class="mt-4 md:mt-0 grid place-items-center col-span-full md:col-span-1">
@if (placeForm.get("image_id")?.value) {
<div class="w-2/3 relative group cursor-pointer" (click)="fileInput.click()">
<div class="w-2/3 relative group cursor-pointer" (click)="imageInput.click()">
<img [src]="placeForm.get('image')?.value"
class="w-full max-h-20 object-cover rounded-full shadow-lg transition-transform duration-300" />
<div
@ -69,7 +80,6 @@
<span class="text-sm text-gray-300">Click to edit</span><i class="pi pi-upload text-white"></i>
</div>
</div>
<input type="file" accept="image/*" #fileInput class="hidden" (change)="onFileSelected($event)" />
} @else {
@if (placeForm.get("image")?.value) {
<div class="w-2/3 relative group cursor-pointer" (click)="clearImage()">
@ -81,7 +91,7 @@
</div>
</div>
} @else {
<div class="w-2/3 relative group cursor-pointer" (click)="fileInput.click()">
<div class="w-2/3 relative group cursor-pointer" (click)="imageInput.click()">
<img src="/favicon.png"
class="w-full max-h-20 object-cover rounded-full shadow-lg transition-transform duration-300" />
<div
@ -89,9 +99,9 @@
<span class="text-sm text-gray-300">Click to edit</span><i class="pi pi-upload text-white"></i>
</div>
</div>
<input type="file" accept="image/*" #fileInput class="hidden" (change)="onFileSelected($event)" />
}
}
<input type="file" accept="image/*" #imageInput class="hidden" (change)="onImageSelected($event)" />
</div>
</div>
</div>

View File

@ -14,8 +14,3 @@
.p-floatlabel:has(.p-inputwrapper-focus) input::placeholder {
color: var(--p-inputtext-placeholder-color) !important;
}
.p-tooltip > .p-tooltip-text {
width: 350px !important;
background: red;
}

View File

@ -91,8 +91,9 @@ export class PlaceCreateModalComponent {
price: "",
allowdog: false,
visited: false,
image: "",
image: null,
image_id: null,
gpx: null,
});
if (this.config.data) {
@ -172,7 +173,7 @@ export class PlaceCreateModalComponent {
this.placeForm.get("name")?.setValue(place);
}
onFileSelected(event: Event) {
onImageSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
@ -201,4 +202,24 @@ export class PlaceCreateModalComponent {
this.placeForm.get("image")?.setValue(this.previous_image);
}
}
onGPXSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = (e) => {
this.placeForm.get("gpx")?.setValue(e.target?.result as string);
this.placeForm.get("gpx")?.markAsDirty();
};
reader.readAsText(file);
}
}
clearGPX() {
this.placeForm.get("gpx")?.setValue(null);
this.placeForm.get("gpx")?.markAsDirty();
}
}

View File

@ -164,6 +164,12 @@ export class ApiService {
);
}
getPlaceGPX(place_id: number): Observable<Place> {
return this.httpClient
.get<Place>(`${this.apiBaseUrl}/places/${place_id}`)
.pipe(map((p) => this._normalizePlaceImage(p)));
}
getTrips(): Observable<TripBase[]> {
return this.httpClient.get<TripBase[]>(`${this.apiBaseUrl}/trips`).pipe(
map((resp) => {

View File

@ -99,3 +99,18 @@ export function placeToMarker(place: Place): L.Marker {
}
return marker;
}
export function gpxToPolyline(gpx: string): L.Polyline {
const parser = new DOMParser();
const gpxDoc = parser.parseFromString(gpx, "application/xml");
const trkpts = Array.from(gpxDoc.querySelectorAll("trkpt"));
const latlngs = trkpts.map((pt) => {
return [
parseFloat(pt.getAttribute("lat")!),
parseFloat(pt.getAttribute("lon")!),
] as [number, number];
});
return L.polyline(latlngs, { color: "blue" });
}