✨ Trip: item image and gpx, Trip: table distance between items
This commit is contained in:
parent
8d9b8f149c
commit
460cdc93fe
@ -111,6 +111,7 @@
|
||||
@if (tripTableSelectedColumns.includes('LatLng')) {<th class="w-12" pResizableColumn>LatLng</th>}
|
||||
@if (tripTableSelectedColumns.includes('price')) {<th class="w-12" pResizableColumn>Price</th>}
|
||||
@if (tripTableSelectedColumns.includes('status')) {<th class="w-12" pResizableColumn>Status</th>}
|
||||
@if (tripTableSelectedColumns.includes('distance')) {<th pResizableColumn>Distance (km)</th>}
|
||||
</tr>
|
||||
</ng-template>
|
||||
@if (tableExpandableMode) {
|
||||
@ -148,10 +149,19 @@
|
||||
</div>
|
||||
} @else {-}
|
||||
</td>}
|
||||
@if (tripTableSelectedColumns.includes('comment')) {<td>
|
||||
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
|
||||
@if (tripitem.image) {
|
||||
<div
|
||||
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||
<img [src]="tripitem.image"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
|
||||
tripitem.comment }}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||
{{ tripitem.comment || '-' }}
|
||||
</div>
|
||||
}
|
||||
</td>}
|
||||
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
|
||||
<div class="print:max-w-full truncate">
|
||||
@ -166,6 +176,9 @@
|
||||
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
|
||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
||||
tripitem.status.label }}</span>}</td>}
|
||||
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
|
||||
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
||||
</td>}
|
||||
</tr>
|
||||
</ng-template>
|
||||
}
|
||||
@ -192,15 +205,24 @@
|
||||
@if (tripitem.place) {
|
||||
<div class="ml-7 print:ml-0 max-w-24 truncate print:whitespace-normal">
|
||||
<img [src]="tripitem.place.image || tripitem.place.category.image"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-9 rounded-full object-cover print:hidden" /> {{
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
|
||||
tripitem.place.name }}
|
||||
</div>
|
||||
} @else {-}
|
||||
</td>}
|
||||
@if (tripTableSelectedColumns.includes('comment')) {<td>
|
||||
@if (tripTableSelectedColumns.includes('comment')) {<td class="relative">
|
||||
@if (tripitem.image) {
|
||||
<div
|
||||
class="ml-7 print:ml-0 truncate print:whitespace-normal line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||
<img [src]="tripitem.image"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 size-9 rounded-full object-cover print:hidden" /> {{
|
||||
tripitem.comment }}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="line-clamp-1 whitespace-pre-line print:line-clamp-none">
|
||||
{{ tripitem.comment || '-' }}
|
||||
</div>
|
||||
}
|
||||
</td>}
|
||||
@if (tripTableSelectedColumns.includes('LatLng')) {<td class="font-mono text-sm">
|
||||
<div class="max-w-20 print:max-w-full truncate">
|
||||
@ -215,6 +237,9 @@
|
||||
[style.background]="tripitem.status.color+'1A'" [style.color]="tripitem.status.color"
|
||||
class="text-xs font-medium me-2 px-2.5 py-0.5 rounded">{{
|
||||
tripitem.status.label }}</span>}</td>}
|
||||
@if (tripTableSelectedColumns.includes('distance')) {<td class="text-sm">
|
||||
<div class="print:max-w-full truncate">{{ tripitem.distance || '-' }}</div>
|
||||
</td>}
|
||||
</tr>
|
||||
</ng-template>
|
||||
}
|
||||
@ -247,7 +272,7 @@
|
||||
<div [ngClass]="{ 'grid col-span-full grid-cols-4': isExpanded, 'flex flex-col sticky': !isExpanded }"
|
||||
class="gap-4 top-4 self-start max-w-screen print:hidden min-w-0">
|
||||
@if (selectedItem) {
|
||||
<div class="p-4 w-full max-w-full min-h-20 md:max-h-[600px] rounded-md shadow text-center">
|
||||
<div class="p-4 w-full max-w-full min-h-20 rounded-md shadow text-center">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div class="p-2 flex items-center gap-4 w-full max-w-full">
|
||||
@if (selectedItem.place) {
|
||||
@ -260,6 +285,9 @@
|
||||
</h1>
|
||||
|
||||
<div class="flex items-center gap-2 flex-none">
|
||||
@if (selectedItem.gpx) {
|
||||
<p-button icon="pi pi-compass" (click)="downloadItemGPX()" text />
|
||||
}
|
||||
@if (selectedItem.lat && selectedItem.lng) {
|
||||
<p-button icon="pi pi-car" (click)="itemToNavigation()" text />
|
||||
}
|
||||
@ -285,9 +313,11 @@
|
||||
</div>
|
||||
|
||||
@if (selectedItem.place) {
|
||||
<div class="rounded-md shadow p-4">
|
||||
<div class="relative rounded-md shadow p-4">
|
||||
<p class="font-bold mb-1">Place</p>
|
||||
<div class="text-sm text-gray-500 truncate">{{ selectedItem.place.name }}</div>
|
||||
<div class="absolute top-2 right-2"><p-button severity="help" text icon="pi pi-pencil"
|
||||
(click)="editPlace(selectedItem.place)" /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -324,6 +354,12 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedItem.image) {
|
||||
<div class="p-4 px-2 gap-4 overflow-auto w-full min-h-0 max-h-64">
|
||||
<img [src]="selectedItem.image" class="w-full object-cover rounded-md" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -379,7 +415,7 @@
|
||||
@defer {
|
||||
@for (p of places; track p.id) {
|
||||
<div class="flex items-center gap-4 py-2 px-4 hover:bg-gray-50 rounded-md overflow-auto dark:hover:bg-gray-800"
|
||||
(mouseenter)="placeHighlightMarker(p.lat, p.lng)" (mouseleave)="resetPlaceHighlightMarker()">
|
||||
(mouseenter)="placeHighlightMarker(p)" (mouseleave)="resetPlaceHighlightMarker()">
|
||||
<img [src]="p.image || p.category.image" class="w-12 rounded-full object-fit">
|
||||
|
||||
<div class="flex flex-col gap-1 truncate">
|
||||
@ -546,8 +582,9 @@
|
||||
<p-dialog header="Packing list" [draggable]="false" [dismissableMask]="true" [modal]="true"
|
||||
[(visible)]="packingDialogVisible" styleClass="w-[95%] md:w-[70%] lg:w-[50%]">
|
||||
<section class="md:max-w-3/4 md:mx-auto max-h-[80%] md:max-h-[600px]">
|
||||
<div class="flex justify-center">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<p-button (click)="addPackingItem()" icon="pi pi-plus" label="Add item" text />
|
||||
<p-button icon="pi pi-ellipsis-h" text />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 mt-4 pb-4">
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
placeToMarker,
|
||||
createClusterGroup,
|
||||
tripDayMarker,
|
||||
gpxToPolyline,
|
||||
} from "../../shared/map";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog";
|
||||
@ -59,6 +60,7 @@ import { CheckboxChangeEvent, CheckboxModule } from "primeng/checkbox";
|
||||
import { TripCreatePackingModalComponent } from "../../modals/trip-create-packing-modal/trip-create-packing-modal.component";
|
||||
import { TripCreateChecklistModalComponent } from "../../modals/trip-create-checklist-modal/trip-create-checklist-modal.component";
|
||||
import { TripInviteMemberModalComponent } from "../../modals/trip-invite-member-modal/trip-invite-member-modal.component";
|
||||
import { calculateDistanceBetween } from "../../shared/haversine";
|
||||
|
||||
@Component({
|
||||
selector: "app-trip",
|
||||
@ -113,6 +115,7 @@ export class TripComponent implements AfterViewInit {
|
||||
map?: L.Map;
|
||||
markerClusterGroup?: L.MarkerClusterGroup;
|
||||
tripMapTemporaryMarker?: L.Marker;
|
||||
tripMapGpxLayer?: L.Layer;
|
||||
tripMapHoveredElement?: HTMLElement;
|
||||
tripMapAntLayer?: L.FeatureGroup;
|
||||
tripMapAntLayerDayID?: number;
|
||||
@ -262,6 +265,7 @@ export class TripComponent implements AfterViewInit {
|
||||
"LatLng",
|
||||
"price",
|
||||
"status",
|
||||
"distance",
|
||||
];
|
||||
tripTableSelectedColumns: string[] = [
|
||||
"day",
|
||||
@ -409,6 +413,7 @@ export class TripComponent implements AfterViewInit {
|
||||
|
||||
flattenTripDayItems(searchValue?: string) {
|
||||
this.sortTripDays();
|
||||
let prevLat: number, prevLng: number;
|
||||
this.flattenedTripItems = this.trip!.days.flatMap((day) =>
|
||||
[...day.items]
|
||||
.filter((item) =>
|
||||
@ -419,20 +424,39 @@ export class TripComponent implements AfterViewInit {
|
||||
: true,
|
||||
)
|
||||
.sort((a, b) => a.time.localeCompare(b.time))
|
||||
.map((item) => ({
|
||||
td_id: day.id,
|
||||
td_label: day.label,
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
text: item.text,
|
||||
status: this.statusToTripStatus(item.status as string),
|
||||
comment: item.comment,
|
||||
price: item.price || undefined,
|
||||
day_id: item.day_id,
|
||||
place: item.place,
|
||||
lat: item.lat || (item.place ? item.place.lat : undefined),
|
||||
lng: item.lng || (item.place ? item.place.lng : undefined),
|
||||
})),
|
||||
.map((item) => {
|
||||
const lat = item.lat ?? (item.place ? item.place.lat : undefined);
|
||||
const lng = item.lng ?? (item.place ? item.place.lng : undefined);
|
||||
|
||||
let distance: number | undefined;
|
||||
if (lat && lng) {
|
||||
if (prevLat && prevLng) {
|
||||
const d = calculateDistanceBetween(prevLat, prevLng, lat, lng);
|
||||
distance = +(Math.round(d * 1000) / 1000).toFixed(2);
|
||||
}
|
||||
prevLat = lat;
|
||||
prevLng = lng;
|
||||
}
|
||||
|
||||
return {
|
||||
td_id: day.id,
|
||||
td_label: day.label,
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
text: item.text,
|
||||
status: this.statusToTripStatus(item.status as string),
|
||||
comment: item.comment,
|
||||
price: item.price || undefined,
|
||||
day_id: item.day_id,
|
||||
place: item.place,
|
||||
image: item.image,
|
||||
image_id: item.image_id,
|
||||
gpx: item.gpx,
|
||||
lat,
|
||||
lng,
|
||||
distance,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -517,30 +541,39 @@ export class TripComponent implements AfterViewInit {
|
||||
this.map?.removeLayer(this.tripMapTemporaryMarker);
|
||||
this.tripMapTemporaryMarker = undefined;
|
||||
}
|
||||
|
||||
if (this.tripMapGpxLayer) {
|
||||
this.map?.removeLayer(this.tripMapGpxLayer);
|
||||
this.tripMapGpxLayer = undefined;
|
||||
}
|
||||
this.resetMapBounds();
|
||||
}
|
||||
|
||||
placeHighlightMarker(lat: number, lng: number) {
|
||||
placeHighlightMarker(item: any) {
|
||||
if (this.tripMapHoveredElement || this.tripMapTemporaryMarker)
|
||||
this.resetPlaceHighlightMarker();
|
||||
|
||||
let marker: L.Marker | undefined;
|
||||
this.markerClusterGroup?.eachLayer((layer: any) => {
|
||||
if (layer.getLatLng && layer.getLatLng().equals([lat, lng])) {
|
||||
if (layer.getLatLng && layer.getLatLng().equals([item.lat, item.lng])) {
|
||||
marker = layer;
|
||||
}
|
||||
});
|
||||
|
||||
if (item.gpx) {
|
||||
this.tripMapGpxLayer = gpxToPolyline(item.gpx);
|
||||
this.tripMapGpxLayer.addTo(this.map!);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
// TripItem without place, but latlng
|
||||
const item = {
|
||||
text: this.selectedItem?.text || "",
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
time: this.selectedItem?.time || "",
|
||||
};
|
||||
this.tripMapTemporaryMarker = tripDayMarker(item).addTo(this.map!);
|
||||
this.map?.fitBounds([[lat, lng]], { padding: [60, 60] });
|
||||
if (this.tripMapGpxLayer) {
|
||||
this.map?.fitBounds(
|
||||
[[item.lat, item.lng], (this.tripMapGpxLayer as any).getBounds()],
|
||||
{ padding: [30, 30] },
|
||||
);
|
||||
} else this.map?.fitBounds([[item.lat, item.lng]], { padding: [60, 60] });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -602,6 +635,7 @@ export class TripComponent implements AfterViewInit {
|
||||
isPlace: !!item.place,
|
||||
idx: idx,
|
||||
time: item.time,
|
||||
gpx: item.gpx,
|
||||
};
|
||||
|
||||
if (item.lat && item.lng)
|
||||
@ -680,8 +714,9 @@ export class TripComponent implements AfterViewInit {
|
||||
prevPoint = coords[0];
|
||||
}
|
||||
|
||||
group.forEach((day: any) => {
|
||||
if (!day.isPlace) layGroup.addLayer(tripDayMarker(day));
|
||||
group.forEach((data: any) => {
|
||||
if (!data.isPlace) layGroup.addLayer(tripDayMarker(data));
|
||||
if (data.gpx) layGroup.addLayer(gpxToPolyline(data.gpx));
|
||||
});
|
||||
});
|
||||
|
||||
@ -724,6 +759,7 @@ export class TripComponent implements AfterViewInit {
|
||||
lng: item.lng,
|
||||
isPlace: !!item.place,
|
||||
time: item.time,
|
||||
gpx: item.gpx,
|
||||
};
|
||||
if (item.place && item.place)
|
||||
return {
|
||||
@ -732,6 +768,7 @@ export class TripComponent implements AfterViewInit {
|
||||
lng: item.place.lng,
|
||||
isPlace: true,
|
||||
time: item.time,
|
||||
gpx: item.gpx,
|
||||
};
|
||||
return undefined;
|
||||
})
|
||||
@ -769,6 +806,7 @@ export class TripComponent implements AfterViewInit {
|
||||
layGroup.addLayer(path);
|
||||
items.forEach((item) => {
|
||||
if (!item.isPlace) layGroup.addLayer(tripDayMarker(item));
|
||||
if (item.gpx) layGroup.addLayer(gpxToPolyline(item.gpx));
|
||||
});
|
||||
|
||||
if (this.tripMapAntLayer) {
|
||||
@ -790,7 +828,7 @@ export class TripComponent implements AfterViewInit {
|
||||
this.resetPlaceHighlightMarker();
|
||||
} else {
|
||||
this.selectedItem = item;
|
||||
if (item.lat && item.lng) this.placeHighlightMarker(item.lat, item.lng);
|
||||
if (item.lat && item.lng) this.placeHighlightMarker(item);
|
||||
}
|
||||
}
|
||||
|
||||
@ -809,7 +847,7 @@ export class TripComponent implements AfterViewInit {
|
||||
|
||||
this.resetPlaceHighlightMarker();
|
||||
this.selectedItem = item;
|
||||
this.placeHighlightMarker(item.lat!, item.lng!);
|
||||
this.placeHighlightMarker(item);
|
||||
}
|
||||
|
||||
deleteTrip() {
|
||||
@ -942,6 +980,18 @@ export class TripComponent implements AfterViewInit {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
downloadItemGPX() {
|
||||
if (!this.selectedItem?.gpx) return;
|
||||
const dataBlob = new Blob([this.selectedItem.gpx]);
|
||||
const downloadURL = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadURL;
|
||||
link.download = `TRIP_${this.trip?.name}_${this.selectedItem.text}.gpx`;
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(downloadURL);
|
||||
}
|
||||
|
||||
tripToNavigation() {
|
||||
// TODO: More services
|
||||
const items = this.flattenedTripItems.filter(
|
||||
@ -1284,6 +1334,47 @@ export class TripComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
editPlace(pEdit: Place) {
|
||||
const modal: DynamicDialogRef = this.dialogService.open(
|
||||
PlaceCreateModalComponent,
|
||||
{
|
||||
header: "Edit Place",
|
||||
modal: true,
|
||||
appendTo: "body",
|
||||
closable: true,
|
||||
dismissableMask: true,
|
||||
width: "55vw",
|
||||
breakpoints: {
|
||||
"1920px": "70vw",
|
||||
"1260px": "90vw",
|
||||
},
|
||||
data: {
|
||||
place: { ...pEdit, category: pEdit.category.id },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
modal.onClose.pipe(take(1)).subscribe({
|
||||
next: (p: Place | null) => {
|
||||
if (!p) return;
|
||||
|
||||
this.apiService
|
||||
.putPlace(p.id, p)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (place: Place) => {
|
||||
const places = [...this.places];
|
||||
const idx = places.findIndex((p) => p.id == place.id);
|
||||
if (idx > -1) places.splice(idx, 1, place);
|
||||
places.push(place);
|
||||
places.sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (this.selectedItem?.place) this.selectedItem.place = place;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateItemFromTrip(old: TripItem, updated: TripItem): void {
|
||||
if (!this.trip) return;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
@if (itemForm.get('id')?.value !== -1) {
|
||||
<p-floatlabel variant="in" class="min-w-0">
|
||||
<p-select [options]="days" optionValue="id" optionLabel="label" inputId="day_id" id="day_id"
|
||||
scrollHeight="320px" appendTo="body" formControlName="day_id" [checkmark]="true" fluid>
|
||||
scrollHeight="320px" appendTo="body" formControlName="day_id" [checkmark]="true" [filter]="true" fluid>
|
||||
<ng-template let-day #item>
|
||||
<div class="whitespace-normal">{{ day.label }}</div>
|
||||
</ng-template>
|
||||
@ -14,7 +14,7 @@
|
||||
} @else {
|
||||
<p-multiselect [options]="days" optionValue="id" optionLabel="label" formControlName="day_id" placeholder="Days"
|
||||
[filter]="false" [showToggleAll]="false" display="chip" styleClass="flex items-center" scrollHeight="320px"
|
||||
appendTo="body" selectedItemsLabel="{0} days selected" fluid>
|
||||
appendTo="body" selectedItemsLabel="{0} days selected" [filter]="true" fluid>
|
||||
<ng-template let-day #item>
|
||||
<div class="whitespace-normal">{{ day.label }}</div>
|
||||
</ng-template>
|
||||
@ -34,8 +34,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid md:grid-cols-7 gap-4">
|
||||
<p-floatlabel variant="in" class="md:col-span-2">
|
||||
<div class="mt-4 grid md:grid-cols-7 grid-cols-2 gap-4">
|
||||
<p-floatlabel variant="in" class="col-span-2">
|
||||
<p-select [options]="places" appendTo="body" optionValue="id" optionLabel="name" inputId="place" id="place"
|
||||
[filter]="true" filterBy="name" formControlName="place" [showClear]="true" class="capitalize" fluid>
|
||||
|
||||
@ -84,11 +84,38 @@
|
||||
</p-floatlabel>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p-floatlabel variant="in" class="w-full">
|
||||
<textarea pTextarea id="comment" formControlName="comment" rows="5" autoResize fluid></textarea>
|
||||
<div class="mt-4 grid col-span-full md:grid-cols-7">
|
||||
<p-floatlabel variant="in" class="col-span-full md:col-span-5">
|
||||
<textarea pTextarea id="comment" formControlName="comment" rows="3" autoResize fluid></textarea>
|
||||
<label for="comment">Comment</label>
|
||||
</p-floatlabel>
|
||||
|
||||
<div class="mt-4 md:mt-0 grid place-items-center">
|
||||
@if (itemForm.get("image")?.value) {
|
||||
<div class="w-2/3 relative group cursor-pointer" (click)="clearImage()">
|
||||
<img [src]="itemForm.get('image')?.value"
|
||||
class="w-full max-h-20 object-cover rounded-md shadow-lg transition-transform duration-300" />
|
||||
<div
|
||||
class="absolute inset-0 bg-black/50 rounded-md flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<i class="pi pi-trash text-white text-3xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<p-button text label="Select image" icon="pi pi-image" (click)="imageInput.click()" />
|
||||
}
|
||||
<input type="file" accept="image/*" #imageInput class="hidden" (change)="onImageSelected($event)" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 md:mt-0 grid place-items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
@if (itemForm.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>
|
||||
</div>
|
||||
|
||||
@ -96,4 +123,15 @@
|
||||
<p-button (click)="closeDialog()" [disabled]="!itemForm.dirty || !itemForm.valid">{{ itemForm.get("id")?.value
|
||||
!== -1 ? "Update" : "Create" }}</p-button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<p-popover #op>
|
||||
<span class="font-medium block mb-2">Members</span>
|
||||
<div class="flex flex-col gap-2">
|
||||
@for (member of members; track member) {
|
||||
<div class="flex items-center gap-2 p-2 hover:bg-emphasis cursor-pointer rounded-border"
|
||||
[class.font-semibold]="selectedPriceMember == member.user" (click)="selectPriceMember(member.user)">{{ member.user
|
||||
}}</div>
|
||||
}
|
||||
</div>
|
||||
</p-popover>
|
||||
@ -46,6 +46,8 @@ export class TripCreateDayItemModalComponent {
|
||||
days: TripDay[] = [];
|
||||
places: Place[] = [];
|
||||
statuses: TripStatus[] = [];
|
||||
previous_image_id: number | null = null;
|
||||
previous_image: string | null = null;
|
||||
|
||||
constructor(
|
||||
private ref: DynamicDialogRef,
|
||||
@ -72,6 +74,9 @@ export class TripCreateDayItemModalComponent {
|
||||
place: null,
|
||||
status: null,
|
||||
price: null,
|
||||
image: null,
|
||||
image_id: null,
|
||||
gpx: null,
|
||||
lat: [
|
||||
"",
|
||||
{
|
||||
@ -156,7 +161,64 @@ export class TripCreateDayItemModalComponent {
|
||||
ret["lat"] = null;
|
||||
ret["lng"] = null;
|
||||
}
|
||||
if (ret["image_id"]) {
|
||||
delete ret["image"];
|
||||
delete ret["image_id"];
|
||||
}
|
||||
if (ret["gpx"] == "1") delete ret["gpx"];
|
||||
if (!ret["place"]) delete ret["place"];
|
||||
this.ref.close(ret);
|
||||
}
|
||||
|
||||
onImageSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files?.length) {
|
||||
const file = input.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
if (this.itemForm.get("image_id")?.value) {
|
||||
this.previous_image_id = this.itemForm.get("image_id")?.value;
|
||||
this.previous_image = this.itemForm.get("image")?.value;
|
||||
this.itemForm.get("image_id")?.setValue(null);
|
||||
}
|
||||
|
||||
this.itemForm.get("image")?.setValue(e.target?.result as string);
|
||||
this.itemForm.get("image")?.markAsDirty();
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
clearImage() {
|
||||
this.itemForm.get("image")?.setValue(null);
|
||||
this.itemForm.get("image_id")?.setValue(null);
|
||||
this.itemForm.markAsDirty();
|
||||
|
||||
if (this.previous_image && this.previous_image_id) {
|
||||
this.itemForm.get("image_id")?.setValue(this.previous_image_id);
|
||||
this.itemForm.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.itemForm.get("gpx")?.setValue(e.target?.result as string);
|
||||
this.itemForm.get("gpx")?.markAsDirty();
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
clearGPX() {
|
||||
this.itemForm.get("gpx")?.setValue(null);
|
||||
this.itemForm.get("gpx")?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
20
src/src/app/shared/haversine.ts
Normal file
20
src/src/app/shared/haversine.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export function calculateDistanceBetween(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number,
|
||||
) {
|
||||
// returns d in meter
|
||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const rLat1 = toRad(lat1);
|
||||
const rLat2 = toRad(lat2);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(rLat1) * Math.cos(rLat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const R = 6371;
|
||||
return R * c;
|
||||
}
|
||||
@ -44,6 +44,9 @@ export interface TripItem {
|
||||
price?: number;
|
||||
day_id: number;
|
||||
status?: string | TripStatus;
|
||||
image?: string;
|
||||
image_id?: number;
|
||||
gpx?: string;
|
||||
}
|
||||
|
||||
export interface TripStatus {
|
||||
@ -64,6 +67,10 @@ export interface FlattenedTripItem {
|
||||
lng?: number;
|
||||
day_id: number;
|
||||
status?: TripStatus;
|
||||
distance?: number;
|
||||
image?: string;
|
||||
image_id?: number;
|
||||
gpx?: string;
|
||||
}
|
||||
|
||||
export interface TripMember {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user