mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
366 feature request memories should not depend on existing visits and trips 2 (#368)
This commit is contained in:
@@ -7,6 +7,8 @@ import com.dedicatedcode.reitti.model.security.MagicLinkAccessLevel;
|
||||
import com.dedicatedcode.reitti.model.security.MagicLinkResourceType;
|
||||
import com.dedicatedcode.reitti.model.security.TokenUser;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.MemoryTripJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.MemoryVisitJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.ProcessedVisitJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TripJdbcService;
|
||||
import com.dedicatedcode.reitti.service.MemoryService;
|
||||
@@ -36,13 +38,17 @@ public class MemoryBlockController {
|
||||
private final ImmichIntegrationService immichIntegrationService;
|
||||
private final TripJdbcService tripJdbcService;
|
||||
private final ProcessedVisitJdbcService processedVisitJdbcService;
|
||||
private final MemoryVisitJdbcService memoryVisitJdbcService;
|
||||
private final MemoryTripJdbcService memoryTripJdbcService;
|
||||
private final StorageService storageService;
|
||||
|
||||
public MemoryBlockController(MemoryService memoryService, ImmichIntegrationService immichIntegrationService, TripJdbcService tripJdbcService, ProcessedVisitJdbcService processedVisitJdbcService, StorageService storageService) {
|
||||
public MemoryBlockController(MemoryService memoryService, ImmichIntegrationService immichIntegrationService, TripJdbcService tripJdbcService, ProcessedVisitJdbcService processedVisitJdbcService, MemoryVisitJdbcService memoryVisitJdbcService, MemoryTripJdbcService memoryTripJdbcService, StorageService storageService) {
|
||||
this.memoryService = memoryService;
|
||||
this.immichIntegrationService = immichIntegrationService;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.processedVisitJdbcService = processedVisitJdbcService;
|
||||
this.memoryVisitJdbcService = memoryVisitJdbcService;
|
||||
this.memoryTripJdbcService = memoryTripJdbcService;
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
@@ -149,6 +155,7 @@ public class MemoryBlockController {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Cluster block not found"));
|
||||
|
||||
MemoryClusterBlock updated = block.withPartIds(selectedParts).withTitle(title);
|
||||
//check if partId is known, else fetch visit or trip and create a new corrosponding MemoryVisit or MemoryTrip depending on the type, and take this id as partId
|
||||
memoryService.updateClusterBlock(user, updated);
|
||||
|
||||
model.addAttribute("memory", memory);
|
||||
@@ -173,11 +180,9 @@ public class MemoryBlockController {
|
||||
Memory memory = memoryService.getMemoryById(user, memoryId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Memory not found"));
|
||||
|
||||
MemoryBlock block = memoryService.addBlock(user, memoryId, position, type);
|
||||
MemoryClusterBlock clusterBlock = new MemoryClusterBlock(block.getId(), selectedParts, title, null, type);
|
||||
memoryService.createClusterBlock(user, clusterBlock);
|
||||
MemoryClusterBlock clusterBlock = memoryService.createClusterBlock(user,memory, title, position, type, selectedParts);
|
||||
model.addAttribute("memory", memory);
|
||||
model.addAttribute("blocks", List.of(this.memoryService.getBlock(user, timezone, memoryId, block.getId()).orElseThrow(() -> new IllegalArgumentException("Block not found"))));
|
||||
model.addAttribute("blocks", List.of(this.memoryService.getBlock(user, timezone, memoryId, clusterBlock.getBlockId()).orElseThrow(() -> new IllegalArgumentException("Block not found"))));
|
||||
model.addAttribute("isOwner", isOwner(memory, user));
|
||||
model.addAttribute("canEdit", canEdit(memory, user));
|
||||
return "memories/view :: view-block";
|
||||
|
||||
@@ -48,14 +48,6 @@ public class LocationDataApiController {
|
||||
this.userJdbcService = userJdbcService;
|
||||
}
|
||||
|
||||
private static LocationPoint toLocationPoint(RawLocationPoint point) {
|
||||
LocationPoint p = new LocationPoint();
|
||||
p.setLatitude(point.getLatitude());
|
||||
p.setLongitude(point.getLongitude());
|
||||
p.setAccuracyMeters(point.getAccuracyMeters());
|
||||
p.setTimestamp(point.getTimestamp().toString());
|
||||
return p;
|
||||
}
|
||||
|
||||
@GetMapping("/raw-location-points/trips")
|
||||
public ResponseEntity<?> getRawLocationPointsTrips(@AuthenticationPrincipal User user,
|
||||
@@ -77,7 +69,7 @@ public class LocationDataApiController {
|
||||
RawLocationDataResponse result = new RawLocationDataResponse(tmp.stream().map(s -> {
|
||||
List<LocationPoint> simplifiedPoints = simplificationService.simplifyPoints(s, zoom);
|
||||
return new RawLocationDataResponse.Segment(simplifiedPoints);
|
||||
}).toList(), latest.map(LocationDataApiController::toLocationPoint).orElse(null));
|
||||
}).toList(), latest.map(this::toLocationPoint).orElse(null));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@@ -151,7 +143,7 @@ public class LocationDataApiController {
|
||||
}).toList();
|
||||
|
||||
Optional<RawLocationPoint> latest = this.rawLocationPointJdbcService.findLatest(user);
|
||||
return ResponseEntity.ok(new RawLocationDataResponse(result, latest.map(LocationDataApiController::toLocationPoint).orElse(null)));
|
||||
return ResponseEntity.ok(new RawLocationDataResponse(result, latest.map(this::toLocationPoint).orElse(null)));
|
||||
|
||||
} catch (DateTimeParseException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
@@ -253,5 +245,13 @@ public class LocationDataApiController {
|
||||
point.getLongitude() <= maxLng;
|
||||
}
|
||||
|
||||
private LocationPoint toLocationPoint(RawLocationPoint point) {
|
||||
LocationPoint p = new LocationPoint();
|
||||
p.setLatitude(point.getLatitude());
|
||||
p.setLongitude(point.getLongitude());
|
||||
p.setAccuracyMeters(point.getAccuracyMeters());
|
||||
p.setTimestamp(point.getTimestamp().toString());
|
||||
return p;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class MemoryClusterBlock implements MemoryBlockPart, Serializable {
|
||||
public String toString() {
|
||||
return "MemoryClusterBlock{" +
|
||||
"blockId=" + blockId +
|
||||
", tripIds=" + partIds +
|
||||
", partIds=" + partIds +
|
||||
", title='" + title + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
'}';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.dedicatedcode.reitti.model.memory;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.Trip;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
public class MemoryTrip {
|
||||
private final Long id;
|
||||
private final boolean connected;
|
||||
private final MemoryVisit startVisit;
|
||||
private final MemoryVisit endVisit;
|
||||
|
||||
private final Instant startTime;
|
||||
private final Instant endTime;
|
||||
|
||||
public static MemoryTrip create(Trip trip) {
|
||||
return new MemoryTrip(null, true, MemoryVisit.create(trip.getStartVisit()), MemoryVisit.create(trip.getEndVisit()), trip.getStartTime(), trip.getEndTime());
|
||||
}
|
||||
|
||||
public MemoryTrip(Long id, boolean connected, MemoryVisit startVisit, MemoryVisit endVisit, Instant startTime, Instant endTime) {
|
||||
this.id = id;
|
||||
this.connected = connected;
|
||||
this.startVisit = startVisit;
|
||||
this.endVisit = endVisit;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
public Instant getStartTime() {
|
||||
return this.startTime;
|
||||
}
|
||||
|
||||
public Instant getEndTime() {
|
||||
return this.endTime;
|
||||
}
|
||||
|
||||
public long getDurationSeconds() {
|
||||
return Duration.between(startTime, endTime).getSeconds();
|
||||
}
|
||||
|
||||
public MemoryVisit getStartVisit() {
|
||||
return this.startVisit;
|
||||
}
|
||||
|
||||
public MemoryVisit getEndVisit() {
|
||||
return this.endVisit;
|
||||
}
|
||||
|
||||
public MemoryTrip withId(Long generatedId) {
|
||||
return new MemoryTrip(generatedId, this.connected, this.startVisit, this.endVisit, this.startTime, this.endTime);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.dedicatedcode.reitti.model.memory;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.Trip;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
@@ -11,14 +10,14 @@ import java.util.Objects;
|
||||
public class MemoryTripClusterBlockDTO implements MemoryBlockPart, Serializable {
|
||||
|
||||
private final MemoryClusterBlock clusterBlock;
|
||||
private final List<Trip> trips;
|
||||
private final List<MemoryTrip> trips;
|
||||
private final String rawLocationPointsUrl;
|
||||
private final LocalDateTime adjustedStartTime;
|
||||
private final LocalDateTime adjustedEndTime;
|
||||
private final Long completeDuration;
|
||||
private final Long movingDuration;
|
||||
|
||||
public MemoryTripClusterBlockDTO(MemoryClusterBlock clusterBlock, List<Trip> trips, String rawLocationPointsUrl, LocalDateTime adjustedStartTime, LocalDateTime adjustedEndTime, Long completeDuration, Long movingDuration) {
|
||||
public MemoryTripClusterBlockDTO(MemoryClusterBlock clusterBlock, List<MemoryTrip> trips, String rawLocationPointsUrl, LocalDateTime adjustedStartTime, LocalDateTime adjustedEndTime, Long completeDuration, Long movingDuration) {
|
||||
this.clusterBlock = clusterBlock;
|
||||
this.trips = trips != null ? List.copyOf(trips) : List.of();
|
||||
this.rawLocationPointsUrl = rawLocationPointsUrl;
|
||||
@@ -32,7 +31,7 @@ public class MemoryTripClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
return clusterBlock;
|
||||
}
|
||||
|
||||
public List<Trip> getTrips() {
|
||||
public List<MemoryTrip> getTrips() {
|
||||
return trips;
|
||||
}
|
||||
|
||||
@@ -61,7 +60,7 @@ public class MemoryTripClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Instant getCombinedStartTime() {
|
||||
if (trips == null || trips.isEmpty()) return null;
|
||||
return trips.stream()
|
||||
.map(Trip::getStartTime)
|
||||
.map(MemoryTrip::getStartTime)
|
||||
.min(Instant::compareTo)
|
||||
.orElse(null);
|
||||
}
|
||||
@@ -69,7 +68,7 @@ public class MemoryTripClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Instant getCombinedEndTime() {
|
||||
if (trips == null || trips.isEmpty()) return null;
|
||||
return trips.stream()
|
||||
.map(Trip::getEndTime)
|
||||
.map(MemoryTrip::getEndTime)
|
||||
.max(Instant::compareTo)
|
||||
.orElse(null);
|
||||
}
|
||||
@@ -77,21 +76,21 @@ public class MemoryTripClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Long getCombinedDurationSeconds() {
|
||||
if (trips == null || trips.isEmpty()) return 0L;
|
||||
return trips.stream()
|
||||
.mapToLong(Trip::getDurationSeconds)
|
||||
.mapToLong(MemoryTrip::getDurationSeconds)
|
||||
.sum();
|
||||
}
|
||||
|
||||
public List<String> getCombinedStartPlaces() {
|
||||
if (trips == null || trips.isEmpty()) return List.of();
|
||||
return trips.stream()
|
||||
.map(trip -> trip.getStartVisit() != null && trip.getStartVisit().getPlace() != null ? trip.getStartVisit().getPlace().getName() : null)
|
||||
.map(trip -> trip.getStartVisit().getName())
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<String> getCombinedEndPlaces() {
|
||||
if (trips == null || trips.isEmpty()) return List.of();
|
||||
return trips.stream()
|
||||
.map(trip -> trip.getEndVisit() != null && trip.getEndVisit().getPlace() != null ? trip.getEndVisit().getPlace().getName() : null)
|
||||
.map(trip -> trip.getEndVisit().getName())
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.dedicatedcode.reitti.model.memory;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.ProcessedVisit;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public class MemoryVisit {
|
||||
private final Long id;
|
||||
private final boolean connected;
|
||||
private final String name;
|
||||
private final Instant startTime;
|
||||
private final Instant endTime;
|
||||
private final double latitudeCentroid;
|
||||
private final double longitudeCentroid;
|
||||
private final ZoneId timezone;
|
||||
|
||||
public static MemoryVisit create(ProcessedVisit visit) {
|
||||
return new MemoryVisit(null, true, visit.getPlace().getName(), visit.getStartTime(), visit.getEndTime(), visit.getPlace().getLatitudeCentroid(), visit.getPlace().getLongitudeCentroid(), visit.getPlace().getTimezone());
|
||||
}
|
||||
|
||||
public MemoryVisit(Long id, boolean connected, String name, Instant startTime, Instant endTime, double latitudeCentroid, double longitudeCentroid, ZoneId timezone) {
|
||||
this.id = id;
|
||||
this.connected = connected;
|
||||
this.name = name;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.latitudeCentroid = latitudeCentroid;
|
||||
this.longitudeCentroid = longitudeCentroid;
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
public Instant getStartTime() {
|
||||
return this.startTime;
|
||||
}
|
||||
|
||||
public Instant getEndTime() {
|
||||
return this.endTime;
|
||||
}
|
||||
|
||||
public long getDurationSeconds() {
|
||||
return Duration.between(startTime, endTime).getSeconds();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public double getLatitudeCentroid() {
|
||||
return this.latitudeCentroid;
|
||||
}
|
||||
|
||||
public double getLongitudeCentroid() {
|
||||
return this.longitudeCentroid;
|
||||
}
|
||||
|
||||
public ZoneId getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public MemoryVisit withId(Long generatedId) {
|
||||
return new MemoryVisit(generatedId, this.connected, this.name, this.startTime, this.endTime, this.latitudeCentroid, this.longitudeCentroid, timezone);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.dedicatedcode.reitti.model.memory;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.ProcessedVisit;
|
||||
import com.dedicatedcode.reitti.model.geo.Visit;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -12,13 +9,13 @@ import java.util.Objects;
|
||||
public class MemoryVisitClusterBlockDTO implements MemoryBlockPart, Serializable {
|
||||
|
||||
private final MemoryClusterBlock clusterBlock;
|
||||
private final List<ProcessedVisit> visits;
|
||||
private final List<MemoryVisit> visits;
|
||||
private final String rawLocationPointsUrl;
|
||||
private final LocalDateTime adjustedStartTime;
|
||||
private final LocalDateTime adjustedEndTime;
|
||||
private final Long completeDuration;
|
||||
|
||||
public MemoryVisitClusterBlockDTO(MemoryClusterBlock clusterBlock, List<ProcessedVisit> visits, String rawLocationPointsUrl, LocalDateTime adjustedStartTime, LocalDateTime adjustedEndTime, Long completeDuration) {
|
||||
public MemoryVisitClusterBlockDTO(MemoryClusterBlock clusterBlock, List<MemoryVisit> visits, String rawLocationPointsUrl, LocalDateTime adjustedStartTime, LocalDateTime adjustedEndTime, Long completeDuration) {
|
||||
this.clusterBlock = clusterBlock;
|
||||
this.visits = visits != null ? List.copyOf(visits) : List.of();
|
||||
this.rawLocationPointsUrl = rawLocationPointsUrl;
|
||||
@@ -31,7 +28,7 @@ public class MemoryVisitClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
return clusterBlock;
|
||||
}
|
||||
|
||||
public List<ProcessedVisit> getVisits() {
|
||||
public List<MemoryVisit> getVisits() {
|
||||
return visits;
|
||||
}
|
||||
|
||||
@@ -56,7 +53,7 @@ public class MemoryVisitClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Instant getCombinedStartTime() {
|
||||
if (visits == null || visits.isEmpty()) return null;
|
||||
return visits.stream()
|
||||
.map(ProcessedVisit::getStartTime)
|
||||
.map(MemoryVisit::getStartTime)
|
||||
.min(Instant::compareTo)
|
||||
.orElse(null);
|
||||
}
|
||||
@@ -64,7 +61,7 @@ public class MemoryVisitClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Instant getCombinedEndTime() {
|
||||
if (visits == null || visits.isEmpty()) return null;
|
||||
return visits.stream()
|
||||
.map(ProcessedVisit::getEndTime)
|
||||
.map(MemoryVisit::getEndTime)
|
||||
.max(Instant::compareTo)
|
||||
.orElse(null);
|
||||
}
|
||||
@@ -72,7 +69,7 @@ public class MemoryVisitClusterBlockDTO implements MemoryBlockPart, Serializable
|
||||
public Long getCombinedDurationSeconds() {
|
||||
if (visits == null || visits.isEmpty()) return 0L;
|
||||
return visits.stream()
|
||||
.mapToLong(ProcessedVisit::getDurationSeconds)
|
||||
.mapToLong(MemoryVisit::getDurationSeconds)
|
||||
.sum();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class MemoryClusterBlockRepository {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void save(User user, MemoryClusterBlock cluster) {
|
||||
public MemoryClusterBlock save(User user, MemoryClusterBlock cluster) {
|
||||
String sql = "INSERT INTO memory_block_cluster (block_id, part_ids, user_id, title, description, type) VALUES (?, ?::jsonb, ?, ?, ?, ?) " +
|
||||
"ON CONFLICT (block_id) DO UPDATE SET part_ids = EXCLUDED.part_ids, title = EXCLUDED.title, description = EXCLUDED.description, type = EXCLUDED.type";
|
||||
try {
|
||||
@@ -35,6 +35,8 @@ public class MemoryClusterBlockRepository {
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to save MemoryClusterBlock", e);
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
public Optional<MemoryClusterBlock> findByBlockId(User user, Long blockId) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.dedicatedcode.reitti.repository;
|
||||
|
||||
import com.dedicatedcode.reitti.model.memory.MemoryTrip;
|
||||
import com.dedicatedcode.reitti.model.memory.MemoryVisit;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MemoryTripJdbcService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public MemoryTripJdbcService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public MemoryTrip save(User user, MemoryTrip memoryTrip, Long memoryBlockId, Long originalId, Long startVisitId, Long endVisitId) {
|
||||
String sql = """
|
||||
INSERT INTO memory_trips (user_id, original_id, memory_block_id, start_visit_id, end_visit_id, start_time, end_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
""";
|
||||
|
||||
Long generatedId = jdbcTemplate.queryForObject(sql, Long.class,
|
||||
user.getId(),
|
||||
originalId,
|
||||
memoryBlockId,
|
||||
startVisitId,
|
||||
endVisitId,
|
||||
Timestamp.from(memoryTrip.getStartTime()),
|
||||
Timestamp.from(memoryTrip.getEndTime())
|
||||
);
|
||||
|
||||
return memoryTrip.withId(generatedId);
|
||||
}
|
||||
|
||||
public List<MemoryTrip> findByMemoryBlockId(Long memoryBlockId) {
|
||||
String sql = """
|
||||
SELECT mt.id, mt.start_time, mt.end_time, mt.original_id,
|
||||
sv.id as start_visit_id, sv.name as start_visit_name, sv.start_time as start_visit_start_time, sv.original_id as start_visit_original_id,
|
||||
sv.end_time as start_visit_end_time, sv.latitude_centroid as start_visit_lat, sv.longitude_centroid as start_visit_lon, sv.timezone as start_visit_timezone,
|
||||
ev.id as end_visit_id, ev.name as end_visit_name, ev.start_time as end_visit_start_time, ev.original_id as end_visit_original_id,
|
||||
ev.end_time as end_visit_end_time, ev.latitude_centroid as end_visit_lat, ev.longitude_centroid as end_visit_lon, ev.timezone as end_visit_timezone
|
||||
FROM memory_trips mt
|
||||
LEFT JOIN memory_visits sv ON mt.start_visit_id = sv.id
|
||||
LEFT JOIN memory_visits ev ON mt.end_visit_id = ev.id
|
||||
WHERE mt.memory_block_id = ?
|
||||
ORDER BY mt.start_time
|
||||
""";
|
||||
|
||||
return jdbcTemplate.query(sql, new MemoryTripRowMapper(), memoryBlockId);
|
||||
}
|
||||
|
||||
public List<MemoryTrip> findByUserAndMemoryId(User user, Long memoryId) {
|
||||
String sql = """
|
||||
SELECT mt.id, mt.start_time, mt.end_time, mt.original_id,
|
||||
sv.id as start_visit_id, sv.name as start_visit_name, sv.start_time as start_visit_start_time,
|
||||
sv.original_id as start_visit_original_id,
|
||||
sv.end_time as start_visit_end_time, sv.latitude_centroid as start_visit_lat, sv.longitude_centroid as start_visit_lon, sv.timezone as start_visit_timezone,
|
||||
ev.id as end_visit_id, ev.name as end_visit_name, ev.start_time as end_visit_start_time, ev.original_id as end_visit_original_id,
|
||||
ev.end_time as end_visit_end_time, ev.latitude_centroid as end_visit_lat, ev.longitude_centroid as end_visit_lon, ev.timezone as end_visit_timezone
|
||||
FROM memory_trips mt
|
||||
LEFT JOIN memory_visits sv ON mt.start_visit_id = sv.id
|
||||
LEFT JOIN memory_visits ev ON mt.end_visit_id = ev.id
|
||||
JOIN memory_block mb ON mt.memory_block_id = mb.id
|
||||
WHERE mt.user_id = ? AND mb.memory_id = ?
|
||||
ORDER BY mt.start_time
|
||||
""";
|
||||
|
||||
return jdbcTemplate.query(sql, new MemoryTripRowMapper(), user.getId(), memoryId);
|
||||
}
|
||||
|
||||
public void deleteByMemoryBlockId(Long memoryBlockId) {
|
||||
String sql = "DELETE FROM memory_trips WHERE memory_block_id = ?";
|
||||
jdbcTemplate.update(sql, memoryBlockId);
|
||||
}
|
||||
|
||||
private static class MemoryTripRowMapper implements RowMapper<MemoryTrip> {
|
||||
@Override
|
||||
public MemoryTrip mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
MemoryVisit startVisit = null;
|
||||
MemoryVisit endVisit = null;
|
||||
|
||||
if (rs.getLong("start_visit_id") != 0) {
|
||||
startVisit = new MemoryVisit(
|
||||
rs.getLong("start_visit_id"),
|
||||
rs.getObject("start_visit_original_id") != null,
|
||||
rs.getString("start_visit_name"),
|
||||
rs.getTimestamp("start_visit_start_time").toInstant(),
|
||||
rs.getTimestamp("start_visit_end_time").toInstant(),
|
||||
rs.getDouble("start_visit_lat"),
|
||||
rs.getDouble("start_visit_lon"),
|
||||
ZoneId.of(rs.getString("start_visit_timezone")));
|
||||
}
|
||||
|
||||
if (rs.getLong("end_visit_id") != 0) {
|
||||
endVisit = new MemoryVisit(
|
||||
rs.getLong("end_visit_id"),
|
||||
rs.getObject("end_visit_original_id") != null,
|
||||
rs.getString("end_visit_name"),
|
||||
rs.getTimestamp("end_visit_start_time").toInstant(),
|
||||
rs.getTimestamp("end_visit_end_time").toInstant(),
|
||||
rs.getDouble("end_visit_lat"),
|
||||
rs.getDouble("end_visit_lon"),
|
||||
ZoneId.of(rs.getString("end_visit_timezone")));
|
||||
}
|
||||
|
||||
return new MemoryTrip(
|
||||
rs.getLong("id"),
|
||||
rs.getObject("original_id") != null,
|
||||
startVisit,
|
||||
endVisit,
|
||||
rs.getTimestamp("start_time").toInstant(),
|
||||
rs.getTimestamp("end_time").toInstant()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.dedicatedcode.reitti.repository;
|
||||
|
||||
import com.dedicatedcode.reitti.model.memory.MemoryVisit;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Time;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MemoryVisitJdbcService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public MemoryVisitJdbcService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public MemoryVisit save(User user, MemoryVisit memoryVisit, Long memoryBlockId, Long originalId) {
|
||||
String sql = """
|
||||
INSERT INTO memory_visits (user_id, original_id, memory_block_id, name, start_time, end_time, latitude_centroid, longitude_centroid, timezone)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
""";
|
||||
|
||||
Long generatedId = jdbcTemplate.queryForObject(sql, Long.class,
|
||||
user.getId(),
|
||||
originalId,
|
||||
memoryBlockId,
|
||||
memoryVisit.getName(),
|
||||
Timestamp.from(memoryVisit.getStartTime()),
|
||||
Time.from(memoryVisit.getEndTime()),
|
||||
memoryVisit.getLatitudeCentroid(),
|
||||
memoryVisit.getLongitudeCentroid(),
|
||||
memoryVisit.getTimezone().getId()
|
||||
);
|
||||
|
||||
return memoryVisit.withId(generatedId);
|
||||
}
|
||||
|
||||
public List<MemoryVisit> findByMemoryBlockId(Long memoryBlockId) {
|
||||
String sql = """
|
||||
SELECT id, name, start_time, end_time, latitude_centroid, longitude_centroid, timezone
|
||||
FROM memory_visits
|
||||
WHERE memory_block_id = ?
|
||||
ORDER BY start_time
|
||||
""";
|
||||
|
||||
return jdbcTemplate.query(sql, new MemoryVisitRowMapper(), memoryBlockId);
|
||||
}
|
||||
|
||||
public void deleteByMemoryBlockId(Long memoryBlockId) {
|
||||
String sql = "DELETE FROM memory_visits WHERE memory_block_id = ?";
|
||||
jdbcTemplate.update(sql, memoryBlockId);
|
||||
}
|
||||
|
||||
private static class MemoryVisitRowMapper implements RowMapper<MemoryVisit> {
|
||||
@Override
|
||||
public MemoryVisit mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return new MemoryVisit(
|
||||
rs.getLong("id"),
|
||||
true, // connected - always true for persisted visits
|
||||
rs.getString("name"),
|
||||
rs.getTimestamp("start_time").toInstant(),
|
||||
rs.getTimestamp("end_time").toInstant(),
|
||||
rs.getDouble("latitude_centroid"),
|
||||
rs.getDouble("longitude_centroid"),
|
||||
ZoneId.of(rs.getString("timezone")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package com.dedicatedcode.reitti.service;
|
||||
|
||||
import com.dedicatedcode.reitti.controller.error.PageNotFoundException;
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.geo.ProcessedVisit;
|
||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
import com.dedicatedcode.reitti.model.geo.Trip;
|
||||
import com.dedicatedcode.reitti.model.memory.*;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
@@ -30,9 +27,11 @@ public class MemoryService {
|
||||
private final MemoryBlockImageGalleryJdbcService memoryBlockImageGalleryJdbcService;
|
||||
private final MemoryClusterBlockRepository memoryClusterBlockRepository;
|
||||
private final MemoryBlockGenerationService blockGenerationService;
|
||||
private final ProcessedVisitJdbcService processedVisitJdbcService;
|
||||
private final TripJdbcService tripJdbcService;
|
||||
private final MemoryVisitJdbcService memoryVisitJdbcService;
|
||||
private final MemoryTripJdbcService memoryTripJdbcService;
|
||||
private final UserSettingsJdbcService userSettingsJdbcService;
|
||||
private final TripJdbcService tripJdbcService;
|
||||
private final ProcessedVisitJdbcService processedVisitJdbcService;
|
||||
|
||||
public MemoryService(
|
||||
MemoryJdbcService memoryJdbcService,
|
||||
@@ -41,18 +40,22 @@ public class MemoryService {
|
||||
MemoryBlockImageGalleryJdbcService memoryBlockImageGalleryJdbcService,
|
||||
MemoryClusterBlockRepository memoryClusterBlockRepository,
|
||||
MemoryBlockGenerationService blockGenerationService,
|
||||
ProcessedVisitJdbcService processedVisitJdbcService,
|
||||
MemoryVisitJdbcService memoryVisitJdbcService,
|
||||
MemoryTripJdbcService memoryTripJdbcService,
|
||||
UserSettingsJdbcService userSettingsJdbcService,
|
||||
TripJdbcService tripJdbcService,
|
||||
UserSettingsJdbcService userSettingsJdbcService) {
|
||||
ProcessedVisitJdbcService processedVisitJdbcService) {
|
||||
this.memoryJdbcService = memoryJdbcService;
|
||||
this.memoryBlockJdbcService = memoryBlockJdbcService;
|
||||
this.memoryBlockTextJdbcService = memoryBlockTextJdbcService;
|
||||
this.memoryBlockImageGalleryJdbcService = memoryBlockImageGalleryJdbcService;
|
||||
this.memoryClusterBlockRepository = memoryClusterBlockRepository;
|
||||
this.blockGenerationService = blockGenerationService;
|
||||
this.processedVisitJdbcService = processedVisitJdbcService;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.memoryVisitJdbcService = memoryVisitJdbcService;
|
||||
this.memoryTripJdbcService = memoryTripJdbcService;
|
||||
this.userSettingsJdbcService = userSettingsJdbcService;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.processedVisitJdbcService = processedVisitJdbcService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -162,8 +165,35 @@ public class MemoryService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createClusterBlock(User user, MemoryClusterBlock clusterBlock) {
|
||||
memoryClusterBlockRepository.save(user, clusterBlock);
|
||||
public MemoryClusterBlock createClusterBlock(User user, Memory memory, String title, int position, BlockType type, List<Long> selectedParts) {
|
||||
MemoryBlock block = addBlock(user, memory.getId(), position, type);
|
||||
List<Long> selectedPartIds = new ArrayList<>();
|
||||
switch (type) {
|
||||
case CLUSTER_TRIP:
|
||||
for (Long partId : selectedParts) {
|
||||
this.tripJdbcService.findById(partId)
|
||||
.map(trip -> {
|
||||
MemoryTrip memoryTrip = MemoryTrip.create(trip);
|
||||
MemoryVisit startVisit = this.memoryVisitJdbcService.save(user, MemoryVisit.create(trip.getStartVisit()), block.getId(), trip.getStartVisit().getId());
|
||||
MemoryVisit endVisit = this.memoryVisitJdbcService.save(user, MemoryVisit.create(trip.getEndVisit()), block.getId(), trip.getEndVisit().getId());
|
||||
return this.memoryTripJdbcService.save(user, memoryTrip, block.getId(), trip.getId(), startVisit.getId(), endVisit.getId());
|
||||
}).map(MemoryTrip::getId).ifPresent(selectedPartIds::add);
|
||||
}
|
||||
break;
|
||||
case CLUSTER_VISIT:
|
||||
for (Long partId : selectedParts) {
|
||||
this.processedVisitJdbcService.findById(partId)
|
||||
.map(visit -> {
|
||||
MemoryVisit memoryVisit = MemoryVisit.create(visit);
|
||||
return this.memoryVisitJdbcService.save(user, memoryVisit, block.getId(), visit.getId());
|
||||
}).map(MemoryVisit::getId).ifPresent(selectedPartIds::add);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid block type");
|
||||
}
|
||||
MemoryClusterBlock clusterBlock = new MemoryClusterBlock(block.getId(), selectedPartIds, title, null, type);
|
||||
return memoryClusterBlockRepository.save(user, clusterBlock);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -234,11 +264,7 @@ public class MemoryService {
|
||||
MemoryBlock memoryBlock = addBlock(user, memoryId, -1, BlockType.IMAGE_GALLERY);
|
||||
memoryBlockImageGalleryJdbcService.create(new MemoryBlockImageGallery(memoryBlock.getId(), imageGalleryBlock.getImages()));
|
||||
} else if (autoGeneratedBlock instanceof MemoryClusterBlock clusterBlock) {
|
||||
MemoryBlock memoryBlock = addBlock(user, memoryId, -1, clusterBlock.getType());
|
||||
memoryClusterBlockRepository.save(user, new MemoryClusterBlock(memoryBlock.getId(),
|
||||
clusterBlock.getPartIds(),
|
||||
clusterBlock.getTitle(),
|
||||
clusterBlock.getDescription(), clusterBlock.getType()));
|
||||
createClusterBlock(user, memory, clusterBlock.getTitle(), -1, clusterBlock.getType(), clusterBlock.getPartIds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,39 +273,39 @@ public class MemoryService {
|
||||
|
||||
|
||||
private Optional<? extends MemoryBlockPart> getClusterTripBlock(User user, ZoneId timezone, MemoryBlock block, UserSettings settings) {
|
||||
Optional<? extends MemoryBlockPart> part;
|
||||
Optional<MemoryClusterBlock> clusterBlockOpt = memoryClusterBlockRepository.findByBlockId(user, block.getId());
|
||||
part = clusterBlockOpt.map(memoryClusterBlock -> {
|
||||
List<Trip> trips = tripJdbcService.findByIds(user, memoryClusterBlock.getPartIds());
|
||||
Optional<Trip> first = trips.stream().findFirst();
|
||||
Optional<Trip> lastTrip = trips.stream().max(Comparator.comparing(Trip::getEndTime));
|
||||
|
||||
long movingTime = trips.stream().mapToLong(Trip::getDurationSeconds).sum();
|
||||
return clusterBlockOpt.map(memoryClusterBlock -> {
|
||||
List<MemoryTrip> trips = memoryTripJdbcService.findByMemoryBlockId(memoryClusterBlock.getBlockId());
|
||||
Optional<MemoryTrip> first = trips.stream().findFirst();
|
||||
Optional<MemoryTrip> lastTrip = trips.stream().max(Comparator.comparing(MemoryTrip::getEndTime));
|
||||
if (first.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
long movingTime = trips.stream().mapToLong(MemoryTrip::getDurationSeconds).sum();
|
||||
long completeTime = first.map(trip -> Duration.between(trip.getStartTime(), lastTrip.get().getEndTime()).toSeconds()).orElse(0L);
|
||||
LocalDateTime adjustedStartTime = first.map(t -> adjustTime(settings, t.getStartTime(), t.getStartVisit().getPlace(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedEndTime = lastTrip.map(t -> adjustTime(settings, t.getEndTime(), t.getEndVisit().getPlace(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedStartTime = first.map(t -> adjustTime(settings, t.getStartTime(), t.getStartVisit().getTimezone(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedEndTime = lastTrip.map(t -> adjustTime(settings, t.getEndTime(), t.getEndVisit().getTimezone(), timezone)).orElse(null);
|
||||
return new MemoryTripClusterBlockDTO(
|
||||
memoryClusterBlock,
|
||||
trips,
|
||||
"/api/v1/raw-location-points/trips?trips=" + String.join(",", memoryClusterBlock.getPartIds().stream().map(Objects::toString).toList()),
|
||||
"/api/v1/raw-location-points?startDate=" + adjustedStartTime + "&endDate=" + adjustedEndTime + "&timezone=" + timezone.getId(),
|
||||
adjustedStartTime,
|
||||
adjustedEndTime,
|
||||
completeTime,
|
||||
movingTime);
|
||||
});
|
||||
return part;
|
||||
}
|
||||
|
||||
private Optional<? extends MemoryBlockPart> getClusterVisitBlock(User user, ZoneId timezone, MemoryBlock block, UserSettings settings) {
|
||||
Optional<? extends MemoryBlockPart> part;
|
||||
Optional<MemoryClusterBlock> clusterVisitBlockOpt = memoryClusterBlockRepository.findByBlockId(user, block.getId());
|
||||
part = clusterVisitBlockOpt.map(memoryClusterBlock -> {
|
||||
List<ProcessedVisit> visits = processedVisitJdbcService.findByIds(user, memoryClusterBlock.getPartIds());
|
||||
Optional<ProcessedVisit> first = visits.stream().findFirst();
|
||||
Optional<ProcessedVisit> last = visits.stream().max(Comparator.comparing(ProcessedVisit::getEndTime));
|
||||
List<MemoryVisit> visits = memoryVisitJdbcService.findByMemoryBlockId(block.getId());
|
||||
Optional<MemoryVisit> first = visits.stream().findFirst();
|
||||
Optional<MemoryVisit> last = visits.stream().max(Comparator.comparing(MemoryVisit::getEndTime));
|
||||
|
||||
LocalDateTime adjustedStartTime = first.map(t -> adjustTime(settings, t.getStartTime(), t.getPlace(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedEndTime = last.map(t -> adjustTime(settings, t.getEndTime(), t.getPlace(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedStartTime = first.map(t -> adjustTime(settings, t.getStartTime(), t.getTimezone(), timezone)).orElse(null);
|
||||
LocalDateTime adjustedEndTime = last.map(t -> adjustTime(settings, t.getEndTime(), t.getTimezone(), timezone)).orElse(null);
|
||||
Long completeDuration = 0L;
|
||||
String rawLocationPointsUrl = first.map(processedVisit -> "/api/v1/raw-location-points?startDate=" + processedVisit.getStartTime().atZone(timezone).toLocalDateTime() + "&endDate=" + last.get().getEndTime().atZone(timezone).toLocalDateTime() + "&timezone=" + timezone).orElse(null);
|
||||
return new MemoryVisitClusterBlockDTO(
|
||||
@@ -293,11 +319,11 @@ public class MemoryService {
|
||||
return part;
|
||||
}
|
||||
|
||||
private LocalDateTime adjustTime(UserSettings settings, Instant startTime, SignificantPlace place, ZoneId timezone) {
|
||||
private LocalDateTime adjustTime(UserSettings settings, Instant startTime, ZoneId placeTimezone, ZoneId timezone) {
|
||||
if (settings.getTimeDisplayMode() == TimeDisplayMode.DEFAULT) {
|
||||
return startTime.atZone(timezone).toLocalDateTime();
|
||||
} else {
|
||||
return startTime.atZone(place.getTimezone()).toLocalDateTime();
|
||||
return startTime.atZone(placeTimezone).toLocalDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE memory_visits
|
||||
(
|
||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
original_id BIGINT NULL REFERENCES processed_visits (id) ON DELETE SET NULL,
|
||||
memory_block_id BIGINT NOT NULL REFERENCES memory_block (id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
latitude_centroid double precision not null,
|
||||
longitude_centroid double precision not null,
|
||||
timezone text not null
|
||||
);
|
||||
|
||||
CREATE TABLE memory_trips
|
||||
(
|
||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
original_id BIGINT NULL REFERENCES trips (id) ON DELETE SET NULL,
|
||||
memory_block_id BIGINT NOT NULL REFERENCES memory_block (id) ON DELETE CASCADE,
|
||||
start_visit_id BIGINT REFERENCES memory_visits (id) ON DELETE CASCADE,
|
||||
end_visit_id BIGINT REFERENCES memory_visits (id) ON DELETE CASCADE,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
@@ -525,6 +525,7 @@ button {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
a.btn:hover,
|
||||
@@ -1971,17 +1972,20 @@ button:disabled {
|
||||
.memories-page .memory-header .action-bar {
|
||||
z-index: 500;
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
background: #00000075;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.memories-page .memory-header:hover .action-bar {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
box-shadow: -1px -1px 9px wheat, 1px -1px 9px #6f560d, -1px 1px 5px #24201c, 1px 1px 0 #171414;
|
||||
}
|
||||
|
||||
.memories-page .memory-header .action-bar div:first-child {
|
||||
@@ -2196,7 +2200,6 @@ button:disabled {
|
||||
/* Lazy loading styles */
|
||||
.lazy-image {
|
||||
transition: opacity 0.3s ease;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.lazy-image.loaded {
|
||||
@@ -2207,7 +2210,7 @@ button:disabled {
|
||||
.gallery-image-item {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -24,47 +24,48 @@
|
||||
<div class="settings-content-area">
|
||||
<div class="memory-header" th:fragment="memory-header">
|
||||
<div class="action-bar">
|
||||
<div sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"><a class="btn"
|
||||
th:href="@{/memories}"><i class="lni lni-arrow-left"></i>
|
||||
<span th:text="#{memory.view.back}"></span></a></a>
|
||||
<div sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')">
|
||||
<a class="btn" th:href="@{/memories}"><i class="lni lni-arrow-left"></i> <span th:text="#{memory.view.back}"></span></a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit}"
|
||||
th:attr="hx-get=@{/memories/{id}/edit(id=${memory.id})}"
|
||||
hx-target=".memory-header"
|
||||
hx-swap="innerHTML"
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
th:text="#{memory.view.edit}">Edit
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-post=@{/memories/{id}/recalculate(id=${memory.id})}"
|
||||
hx-target="body"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Recalculate this memory? This will update all location data."
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
hx-indicator="#memory-processing-overlay"
|
||||
th:text="#{memory.view.recalculate}">Recalculate
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-get=@{/memories/{id}/share(id=${memory.id})}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="lni lni-share"></i> Share
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-danger"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-delete=@{/memories/{id}(id=${memory.id})}"
|
||||
hx-confirm="Are you sure you want to delete this memory? This action cannot be undone."
|
||||
hx-target="body"
|
||||
hx-swap="innerHTML">
|
||||
<i class="lni lni-trash-3"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit}"
|
||||
th:attr="hx-get=@{/memories/{id}/edit(id=${memory.id})}"
|
||||
hx-target=".memory-header"
|
||||
hx-swap="innerHTML"
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
th:text="#{memory.view.edit}">Edit
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-post=@{/memories/{id}/recalculate(id=${memory.id})}"
|
||||
hx-target="body"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Recalculate this memory? This will update all location data."
|
||||
hx-vals="js:{timezone: getUserTimezone()}"
|
||||
hx-indicator="#memory-processing-overlay"
|
||||
th:text="#{memory.view.recalculate}">Recalculate
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-get=@{/memories/{id}/share(id=${memory.id})}"
|
||||
hx-target="#share-overlay-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="lni lni-share"></i> Share
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-danger"
|
||||
th:if="${canEdit && isOwner}"
|
||||
th:attr="hx-delete=@{/memories/{id}(id=${memory.id})}"
|
||||
hx-confirm="Are you sure you want to delete this memory? This action cannot be undone."
|
||||
hx-target="body"
|
||||
hx-swap="innerHTML">
|
||||
<i class="lni lni-trash-3"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="memory-header-section map-header-section">
|
||||
<div id="memory-map" class="memory-map"></div>
|
||||
@@ -115,10 +116,8 @@
|
||||
class="gallery-image-item"
|
||||
th:attr="data-image-url=${image.imageUrl},data-caption=${image.caption}">
|
||||
<div class="photo-loading-spinner"></div>
|
||||
<img th:data-src="${image.imageUrl}"
|
||||
th:alt="${image.caption != null ? image.caption : 'Gallery image'}"
|
||||
<img th:data-src="${image.imageUrl}"
|
||||
class="lazy-image"
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23999'%3ELoading...%3C/text%3E%3C/svg%3E"
|
||||
onload="this.classList.add('loaded'); this.previousElementSibling.style.display='none';"
|
||||
onerror="this.style.display='none'; this.previousElementSibling.style.display='none'; this.parentElement.innerHTML='📷';">
|
||||
</div>
|
||||
@@ -331,10 +330,10 @@
|
||||
if (trips.length > 0) {
|
||||
// Start marker
|
||||
const startTrip = trips[0];
|
||||
if (startTrip.startVisit && startTrip.startVisit.place) {
|
||||
const startLat = startTrip.startVisit.place.latitudeCentroid;
|
||||
const startLon = startTrip.startVisit.place.longitudeCentroid;
|
||||
const startName = startTrip.startVisit.place.name;
|
||||
if (startTrip.startVisit) {
|
||||
const startLat = startTrip.startVisit.latitudeCentroid;
|
||||
const startLon = startTrip.startVisit.longitudeCentroid;
|
||||
const startName = startTrip.startVisit.name;
|
||||
const startTimeFormatted = combinedStartTime ? new Date(combinedStartTime).toLocaleTimeString() : 'unknown';
|
||||
L.circleMarker([startLat, startLon], {
|
||||
color: '#4a9fdc',
|
||||
@@ -347,10 +346,10 @@
|
||||
// Intermediate stops (end of each trip except last)
|
||||
for (let i = 0; i < trips.length - 1; i++) {
|
||||
const trip = trips[i];
|
||||
if (trip.endVisit && trip.endVisit.place) {
|
||||
const lat = trip.endVisit.place.latitudeCentroid;
|
||||
const lon = trip.endVisit.place.longitudeCentroid;
|
||||
const name = trip.endVisit.place.name;
|
||||
if (trip.endVisit) {
|
||||
const lat = trip.endVisit.latitudeCentroid;
|
||||
const lon = trip.endVisit.longitudeCentroid;
|
||||
const name = trip.endVisit.name;
|
||||
const arrivedAfter = combinedStartTime && trip.endTime ? humanizeDuration((new Date(trip.endTime) - new Date(combinedStartTime)),{units: ["h", "m"], round: true}) : 'unknown';
|
||||
L.circleMarker([lat, lon], {
|
||||
color: '#6a6a6a',
|
||||
@@ -363,10 +362,10 @@
|
||||
|
||||
// End marker
|
||||
const endTrip = trips[trips.length - 1];
|
||||
if (endTrip.endVisit && endTrip.endVisit.place) {
|
||||
const endLat = endTrip.endVisit.place.latitudeCentroid;
|
||||
const endLon = endTrip.endVisit.place.longitudeCentroid;
|
||||
const endName = endTrip.endVisit.place.name;
|
||||
if (endTrip.endVisit) {
|
||||
const endLat = endTrip.endVisit.latitudeCentroid;
|
||||
const endLon = endTrip.endVisit.longitudeCentroid;
|
||||
const endName = endTrip.endVisit.name;
|
||||
const arrivedAfter = combinedStartTime && endTrip.endTime ? humanizeDuration((new Date(endTrip.endTime) - new Date(combinedStartTime)),{units: ["h", "m"], round: true}) : 'unknown';
|
||||
L.circleMarker([endLat, endLon], {
|
||||
color: '#37bd57',
|
||||
@@ -395,9 +394,9 @@
|
||||
<h4>Trips in this Journey:</h4>
|
||||
<ul>
|
||||
<li th:each="trip : ${block.trips}" class="trip-item">
|
||||
<span th:text="${trip.startVisit != null && trip.startVisit.place != null ? trip.startVisit.place.name : 'Start'}">Start</span>
|
||||
<span th:text="${trip.startVisit.name ?: 'Start'}">Start</span>
|
||||
<span> → </span>
|
||||
<span th:text="${trip.endVisit != null && trip.endVisit.place != null ? trip.endVisit.place.name : 'End'}">End</span>
|
||||
<span th:text="${trip.endVisit.name ?: 'End'}">End</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -461,18 +460,15 @@
|
||||
let bounds = [];
|
||||
if (visits.length > 0) {
|
||||
for (let i = 0; i < visits.length; i++) {
|
||||
const visit = visits[i];
|
||||
if (visit.place) {
|
||||
const lat = visit.place.latitudeCentroid;
|
||||
const lon = visit.place.longitudeCentroid;
|
||||
const name = visit.place.name;
|
||||
L.circleMarker([lat, lon], {
|
||||
color: '#6a6a6a',
|
||||
fillColor: '#ff984f',
|
||||
fillOpacity: 0.1
|
||||
}).addTo(clusterMap).bindPopup('Name: ' + name);
|
||||
bounds.push([lat, lon]);
|
||||
}
|
||||
const lat = visits[i].latitudeCentroid;
|
||||
const lon = visits[i].longitudeCentroid;
|
||||
const name = visits[i].name;
|
||||
L.circleMarker([lat, lon], {
|
||||
color: '#6a6a6a',
|
||||
fillColor: '#ff984f',
|
||||
fillOpacity: 0.1
|
||||
}).addTo(clusterMap).bindPopup('Name: ' + name);
|
||||
bounds.push([lat, lon]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +484,7 @@
|
||||
<h4>Visits in this Journey:</h4>
|
||||
<ul>
|
||||
<li th:each="visit : ${block.visits}" class="trip-item">
|
||||
<span th:text="${visit.place != null ? visit.place.name : 'Start'}">Start</span>
|
||||
<span th:text="${visit.name ?: 'Start'}">Start</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user