{t('journey.settings.entryFieldsHint')}
diff --git a/client/src/store/journeyStore.ts b/client/src/store/journeyStore.ts index c3427c30e..1faa1b1c2 100644 --- a/client/src/store/journeyStore.ts +++ b/client/src/store/journeyStore.ts @@ -24,6 +24,8 @@ export interface Journey { * boolean would make `=== true` compile and never hold. */ show_trip_tracks?: number + /** Entries without a place take the position of their first geotagged photo (#1003); 0/1, off by default. */ + photo_location?: number /** * Which of the optional entry fields this journey uses (discussion #2299). * diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index 7f0e6de54..31f7dc6a8 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -5440,6 +5440,16 @@ function runMigrations(db: Database.Database): void { if (!has('email')) db.exec('ALTER TABLE places ADD COLUMN email TEXT'); if (!has('opening_hours')) db.exec('ALTER TABLE places ADD COLUMN opening_hours TEXT'); }, + /** + * A journey that puts an entry on the map where its first photo was taken + * (#1003). Off unless the owner turns it on: an entry somebody placed by hand + * is never moved, but an entry left without a place on purpose should not + * grow one either just because a picture was added. + */ + () => { + const cols = db.prepare("SELECT name FROM pragma_table_info('journeys')").all() as { name: string }[]; + if (!cols.some(c => c.name === 'photo_location')) db.exec('ALTER TABLE journeys ADD COLUMN photo_location INTEGER NOT NULL DEFAULT 0'); + }, ]; if (currentVersion < migrations.length) { diff --git a/server/src/nest/journey/journey-domain.service.ts b/server/src/nest/journey/journey-domain.service.ts index 9d786822c..f6c4f8f83 100644 --- a/server/src/nest/journey/journey-domain.service.ts +++ b/server/src/nest/journey/journey-domain.service.ts @@ -353,6 +353,7 @@ export class JourneyDomainService { status: string; status_override: string | null; show_trip_tracks: boolean | number; + photo_location: boolean | number; show_verdict: boolean | number; show_mood: boolean | number; show_weather: boolean | number; @@ -373,13 +374,14 @@ export class JourneyDomainService { 'status', 'status_override', 'show_trip_tracks', + 'photo_location', 'show_verdict', 'show_mood', 'show_weather', ]; // Stored as INTEGER, and better-sqlite3 refuses to bind a JS boolean, so the // flags on this table are coerced rather than passed through. - const BOOLEAN_FIELDS = new Set(['show_trip_tracks', 'show_verdict', 'show_mood', 'show_weather']); + const BOOLEAN_FIELDS = new Set(['show_trip_tracks', 'show_verdict', 'show_mood', 'show_weather', 'photo_location']); const fields: string[] = []; const values: unknown[] = []; for (const [key, val] of Object.entries(data)) { @@ -399,6 +401,50 @@ export class JourneyDomainService { return this.db.prepare('SELECT * FROM journeys WHERE id = ?').get(journeyId) as Journey; } + /** + * Entries without a place that just received a geotagged photo, on journeys + * that asked for it (#1003): each takes the position of its first such photo. + * An entry that already has coordinates is never moved. Answers what changed, + * so the caller can name the places and tell the journeys. + */ + placeEntriesFromPhotos(trekPhotoIds: number[]): { entryId: number; journeyId: number; lat: number; lng: number }[] { + if (!trekPhotoIds.length) return []; + const marks = trekPhotoIds.map(() => '?').join(','); + const rows = this.db.prepare(` + SELECT je.id AS entryId, je.journey_id AS journeyId, tp.lat, tp.lng + FROM trek_photos tp + JOIN journey_photos gp ON gp.photo_id = tp.id + JOIN journey_entry_photos jep ON jep.journey_photo_id = gp.id + JOIN journey_entries je ON je.id = jep.entry_id + JOIN journeys j ON j.id = je.journey_id + WHERE tp.id IN (${marks}) + AND tp.lat IS NOT NULL AND tp.lng IS NOT NULL + AND je.location_lat IS NULL AND je.location_lng IS NULL + AND je.type != 'skeleton' + AND j.photo_location = 1 + ORDER BY je.id, jep.sort_order, jep.journey_photo_id + `).all(...trekPhotoIds) as { entryId: number; journeyId: number; lat: number; lng: number }[]; + const placed: { entryId: number; journeyId: number; lat: number; lng: number }[] = []; + const seen = new Set