Merge remote-tracking branch 'origin/main'

This commit is contained in:
Hosted Weblate
2026-01-02 07:18:28 +01:00
8 changed files with 95 additions and 146 deletions

View File

@@ -14,7 +14,7 @@ public record GeoPoint(double latitude, double longitude) implements Serializabl
}
public boolean near(GeoPoint point) {
return GeoUtils.distanceInMeters(this, point) < 100;
return GeoUtils.distanceInMeters(this, point) < 150;
}
@Override

View File

@@ -14,7 +14,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp;
import java.time.*;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -32,7 +35,7 @@ public class RawLocationPointJdbcService {
public RawLocationPointJdbcService(JdbcTemplate jdbcTemplate, PointReaderWriter pointReaderWriter, GeometryFactory geometryFactory) {
this.jdbcTemplate = jdbcTemplate;
this.rawLocationPointRowMapper = (rs, rowNum) -> new RawLocationPoint(
this.rawLocationPointRowMapper = (rs, _) -> new RawLocationPoint(
rs.getLong("id"),
rs.getTimestamp("timestamp").toInstant(),
pointReaderWriter.read(rs.getString("geom")),
@@ -53,7 +56,7 @@ public class RawLocationPointJdbcService {
User user, Instant startTime, Instant endTime) {
String sql = "SELECT rlp.id, rlp.accuracy_meters, rlp.elevation_meters, rlp.timestamp, rlp.user_id, ST_AsText(rlp.geom) as geom, rlp.processed, rlp.synthetic, rlp.ignored, rlp.version " +
"FROM raw_location_points rlp " +
"WHERE rlp.user_id = ? AND rlp.timestamp BETWEEN ? AND ? " +
"WHERE rlp.user_id = ? AND rlp.timestamp >= ? AND rlp.timestamp < ? " +
"ORDER BY rlp.timestamp";
return jdbcTemplate.query(sql, rawLocationPointRowMapper,
user.getId(), Timestamp.from(startTime), Timestamp.from(endTime));
@@ -71,7 +74,7 @@ public class RawLocationPointJdbcService {
if (!includeIgnored) {
sql.append("AND rlp.ignored = false ");
}
sql.append("AND rlp.timestamp BETWEEN ? AND ? ").append("ORDER BY rlp.timestamp");
sql.append("AND rlp.timestamp >= ? AND rlp.timestamp < ? ").append("ORDER BY rlp.timestamp");
return jdbcTemplate.query(sql.toString(), rawLocationPointRowMapper,
user.getId(), Timestamp.from(startTime), Timestamp.from(endTime));
}
@@ -88,7 +91,7 @@ public class RawLocationPointJdbcService {
if (!includeIgnored) {
sql.append("AND rlp.ignored = false ");
}
sql.append("AND rlp.timestamp BETWEEN ? AND ? ").append("ORDER BY rlp.timestamp")
sql.append("AND rlp.timestamp >= ? AND rlp.timestamp < ? ").append("ORDER BY rlp.timestamp")
.append(" OFFSET ").append(page * pageSize).append(" LIMIT ").append(pageSize);
return jdbcTemplate.query(sql.toString(), rawLocationPointRowMapper,
user.getId(), Timestamp.from(startTime), Timestamp.from(endTime));
@@ -107,7 +110,7 @@ public class RawLocationPointJdbcService {
if (!includeIgnored) {
sql.append("AND rlp.ignored = false ");
}
sql.append("AND rlp.timestamp BETWEEN ? AND ? ");
sql.append("AND rlp.timestamp >= ? AND rlp.timestamp < ? ");
return jdbcTemplate.queryForObject(sql.toString(), Long.class,
user.getId(), Timestamp.from(startTime), Timestamp.from(endTime));
}
@@ -131,7 +134,7 @@ public class RawLocationPointJdbcService {
public RawLocationPoint create(User user, RawLocationPoint rawLocationPoint) {
String sql = "INSERT INTO raw_location_points (user_id, timestamp, accuracy_meters, elevation_meters, geom, processed, synthetic, ignored) " +
"VALUES (?, ?, ?, ?, ST_GeomFromText(?, '4326'), ?, ?, ?) RETURNING id";
"VALUES (?, ?, ?, ?, ST_GeomFromText(?, '4326'), ?, ?, ?) ON CONFLICT DO NOTHING RETURNING id";
Long id = jdbcTemplate.queryForObject(sql, Long.class,
user.getId(),
Timestamp.from(rawLocationPoint.getTimestamp()),
@@ -165,7 +168,7 @@ public class RawLocationPointJdbcService {
"FROM raw_location_points rlp " +
"WHERE rlp.id = ?";
List<RawLocationPoint> results = jdbcTemplate.query(sql, rawLocationPointRowMapper, id);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
return results.isEmpty() ? Optional.empty() : Optional.of(results.getFirst());
}
public Optional<RawLocationPoint> findLatest(User user, Instant since) {
@@ -174,7 +177,7 @@ public class RawLocationPointJdbcService {
"WHERE rlp.user_id = ? AND rlp.timestamp >= ? " +
"ORDER BY rlp.timestamp LIMIT 1";
List<RawLocationPoint> results = jdbcTemplate.query(sql, rawLocationPointRowMapper, user.getId(), Timestamp.from(since));
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
return results.isEmpty() ? Optional.empty() : Optional.of(results.getFirst());
}
public Optional<RawLocationPoint> findLatest(User user) {
@@ -183,7 +186,7 @@ public class RawLocationPointJdbcService {
"WHERE rlp.user_id = ? " +
"ORDER BY rlp.timestamp DESC LIMIT 1";
List<RawLocationPoint> results = jdbcTemplate.query(sql, rawLocationPointRowMapper, user.getId());
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
return results.isEmpty() ? Optional.empty() : Optional.of(results.getFirst());
}
public List<ClusteredPoint> findClusteredPointsInTimeRangeForUser(
@@ -191,7 +194,7 @@ public class RawLocationPointJdbcService {
String sql = "SELECT rlp.id, rlp.accuracy_meters, rlp.elevation_meters, rlp.timestamp, rlp.user_id, ST_AsText(rlp.geom) as geom, rlp.processed, rlp.synthetic, rlp.ignored, rlp.version , " +
"ST_ClusterDBSCAN(rlp.geom, ?, ?) over () AS cluster_id " +
"FROM raw_location_points rlp " +
"WHERE rlp.user_id = ? AND rlp.timestamp BETWEEN ? AND ?";
"WHERE rlp.user_id = ? AND rlp.timestamp >= ? AND rlp.timestamp < ?";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
@@ -234,7 +237,7 @@ public class RawLocationPointJdbcService {
FROM raw_location_points
WHERE user_id = ?
AND ST_Within(geom, ST_MakeEnvelope(?, ?, ?, ?, 4326))
AND timestamp BETWEEN ?::timestamp AND ?::timestamp
AND timestamp >= ?::timestamp AND timestamp < ?::timestamp
AND ignored = false
""";
@@ -245,7 +248,6 @@ public class RawLocationPointJdbcService {
Timestamp.from(endTime)
);
logger.trace("took [{}]ms to count relevant points", (System.nanoTime() - start) / 1_000_000);
// If we have fewer points than the budget, return all without sampling
if (relevantPointCount <= maxPoints) {
String sql = """
@@ -268,7 +270,7 @@ public class RawLocationPointJdbcService {
OVER (ORDER BY timestamp) as next_in_box
FROM raw_location_points
WHERE user_id = ?
AND timestamp BETWEEN ?::timestamp AND ?::timestamp
AND timestamp >= ?::timestamp AND timestamp < ?::timestamp
AND ignored = false
)
SELECT
@@ -323,7 +325,7 @@ public class RawLocationPointJdbcService {
OVER (ORDER BY timestamp) as next_in_box
FROM raw_location_points
WHERE user_id = ?
AND timestamp BETWEEN ?::timestamp AND ?::timestamp
AND timestamp >= ?::timestamp AND timestamp < ?::timestamp
AND ignored = false
),
relevant_points AS (
@@ -406,7 +408,7 @@ public class RawLocationPointJdbcService {
version
FROM raw_location_points
WHERE user_id = ?
AND timestamp BETWEEN ? AND ?
AND timestamp >= ? AND timestamp < ?
AND ignored = false
ORDER BY
date_trunc('hour', timestamp) +
@@ -502,7 +504,7 @@ public class RawLocationPointJdbcService {
}
public boolean containsData(User user, Instant start, Instant end) {
Integer count = this.jdbcTemplate.queryForObject("SELECT count(*) FROM raw_location_points WHERE user_id = ? AND timestamp > ? AND timestamp < ? LIMIT 1",
Integer count = this.jdbcTemplate.queryForObject("SELECT count(*) FROM raw_location_points WHERE user_id = ? AND timestamp >= ? AND timestamp < ? LIMIT 1",
Integer.class,
user.getId(),
start != null ? Timestamp.from(start) : Timestamp.valueOf("1970-01-01 00:00:00"),
@@ -543,7 +545,7 @@ public class RawLocationPointJdbcService {
}
public void deleteSyntheticPointsInRange(User user, Instant start, Instant end) {
String sql = "DELETE FROM raw_location_points WHERE user_id = ? AND timestamp BETWEEN ? AND ? AND synthetic = true";
String sql = "DELETE FROM raw_location_points WHERE user_id = ? AND timestamp >= ? AND timestamp < ? AND synthetic = true";
jdbcTemplate.update(sql, user.getId(), Timestamp.from(start), Timestamp.from(end));
}

View File

@@ -79,7 +79,8 @@ public class DefaultImportProcessor implements ImportProcessor {
@Override
public boolean isIdle() {
return pendingTriggers.isEmpty() || pendingTriggers.values().stream().allMatch(ScheduledFuture::isDone);
return importExecutors.getQueue().isEmpty() &&
pendingTriggers.isEmpty() || pendingTriggers.values().stream().allMatch(ScheduledFuture::isDone);
}
@PreDestroy

View File

@@ -14,6 +14,7 @@ import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@@ -58,24 +59,23 @@ public class LocationDataDensityNormalizer {
TimeRange inputRange = computeTimeRange(newPoints);
// Step 2: Get detection parameters (use the earliest point's time for config lookup)
DetectionParameter detectionParams = visitDetectionParametersService.getCurrentConfiguration(user, inputRange.start);
DetectionParameter detectionParams = visitDetectionParametersService.getCurrentConfiguration(user, inputRange.start());
DetectionParameter.LocationDensity densityConfig = detectionParams.getLocationDensity();
// Step 3: Expand the time range by the interpolation window to catch boundary gaps
Duration window = Duration.ofMinutes(densityConfig.getMaxInterpolationGapMinutes());
long maxInterpolationGapMinutes = densityConfig.getMaxInterpolationGapMinutes();
Duration window = Duration.ofMinutes(maxInterpolationGapMinutes);
TimeRange expandedRange = new TimeRange(
inputRange.start.minus(window),
inputRange.end.plus(window)
inputRange.start().minus(window),
inputRange.end().plus(window)
);
// Step 4: Delete all synthetic points in the expanded range
rawLocationPointService.deleteSyntheticPointsInRange(user, expandedRange.start, expandedRange.end);
rawLocationPointService.deleteSyntheticPointsInRange(user, expandedRange.start(), expandedRange.end());
// Step 5: Fetch all existing points in the expanded range (single DB query)
List<RawLocationPoint> existingPoints = rawLocationPointService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, expandedRange.start, expandedRange.end);
List<RawLocationPoint> existingPoints = rawLocationPointService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, expandedRange.start().minus(maxInterpolationGapMinutes, ChronoUnit.MINUTES), expandedRange.end().plus(maxInterpolationGapMinutes, ChronoUnit.MINUTES));
logger.debug("Found {} existing points in expanded range [{} - {}]",
existingPoints.size(), expandedRange.start, expandedRange.end);
logger.debug("Found {} existing points in expanded range [{} - {}]", existingPoints.size(), expandedRange.start(), expandedRange.end());
// Step 7: Sort deterministically by timestamp, then by ID (for repeatability)
existingPoints.sort(Comparator
@@ -89,18 +89,8 @@ public class LocationDataDensityNormalizer {
// Step 8: Process gaps (generate synthetic points)
processGaps(user, existingPoints, densityConfig);
// Step 9: Re-fetch and handle excess density
// We need to re-fetch because synthetic points were just inserted
List<RawLocationPoint> updatedPoints = rawLocationPointService
.findByUserAndTimestampBetweenOrderByTimestampAsc(user, expandedRange.start, expandedRange.end);
updatedPoints.sort(Comparator
.comparing(RawLocationPoint::getTimestamp)
.thenComparing(p -> p.getGeom().latitude())
.thenComparing(p -> p.getGeom().longitude())
.thenComparing(RawLocationPoint::isSynthetic));
handleExcessDensity(user, updatedPoints);
handleExcessDensity(user, existingPoints);
logger.debug("Completed batch density normalization for user {}", user.getUsername());
@@ -149,29 +139,19 @@ public class LocationDataDensityNormalizer {
long maxInterpolationSeconds = densityConfig.getMaxInterpolationGapMinutes() * 60L;
List<LocationPoint> allSyntheticPoints = new ArrayList<>();
Set<GapKey> processedGaps = new HashSet<>();
for (int i = 0; i < points.size() - 1; i++) {
RawLocationPoint current = points.get(i);
RawLocationPoint next = points.get(i + 1);
// Skip if either point is already ignored
if (current.isIgnored() || next.isIgnored()) {
// Skip if either point is already ignored or synthetic
if (current.isIgnored() || next.isIgnored() || current.isSynthetic()) {
continue;
}
// Create a deterministic gap key to avoid reprocessing
GapKey gapKey = new GapKey(current.getTimestamp(), next.getTimestamp());
if (processedGaps.contains(gapKey)) {
continue;
}
processedGaps.add(gapKey);
long gapSeconds = Duration.between(current.getTimestamp(), next.getTimestamp()).getSeconds();
if (gapSeconds > gapThresholdSeconds && gapSeconds <= maxInterpolationSeconds) {
logger.trace("Found gap of {} seconds between {} and {}",
gapSeconds, current.getTimestamp(), next.getTimestamp());
List<LocationPoint> syntheticPoints = syntheticGenerator.generateSyntheticPoints(
current,
@@ -180,6 +160,9 @@ public class LocationDataDensityNormalizer {
densityConfig.getMaxInterpolationDistanceMeters()
);
logger.trace("Found gap of {} seconds between {} and {} -> created {} synthetic points between them",
gapSeconds, current.getTimestamp(), next.getTimestamp(), syntheticPoints.size());
allSyntheticPoints.addAll(syntheticPoints);
}
}
@@ -288,43 +271,5 @@ public class LocationDataDensityNormalizer {
}
/**
* Represents a time range with start and end instants.
*/
private static class TimeRange {
final Instant start;
final Instant end;
TimeRange(Instant start, Instant end) {
this.start = start;
this.end = end;
}
}
/**
* Represents a unique gap between two timestamps.
* Used to avoid processing the same gap multiple times.
*/
private static class GapKey {
final Instant start;
final Instant end;
GapKey(Instant start, Instant end) {
this.start = start;
this.end = end;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GapKey gapKey = (GapKey) o;
return Objects.equals(start, gapKey.start) && Objects.equals(end, gapKey.end);
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
}
public record TimeRange(Instant start, Instant end) {}
}

View File

@@ -5,6 +5,7 @@ import com.dedicatedcode.reitti.model.geo.GeoPoint;
public class TestConstants {
public static class Points {
public static final GeoPoint MOLTKESTR = new GeoPoint(53.863149, 10.700927);
public static final GeoPoint MOLTKEPLATZ = new GeoPoint(53.863352275279084,10.707716378325035);
public static final GeoPoint DIELE = new GeoPoint(53.868977437711706, 10.680643284930158);
public static final GeoPoint OBI = new GeoPoint(53.87172110622166, 10.747495611916795);
public static final GeoPoint GARTEN = new GeoPoint(53.87318065243313, 10.732683669587999);

View File

@@ -64,9 +64,9 @@ class LocationDataDensityNormalizerTest {
List<RawLocationPoint> storedPoints = this.rawLocationPointService.findByUserAndProcessedIsFalseOrderByTimestampWithLimit(testUser, 1000, 0);
assertEquals(8, storedPoints.size());
assertEquals(26, storedPoints.size());
assertEquals(0, storedPoints.stream().filter(RawLocationPoint::isIgnored).count());
assertEquals(2, storedPoints.stream().filter(RawLocationPoint::isSynthetic).count());
assertEquals(20, storedPoints.stream().filter(RawLocationPoint::isSynthetic).count());
}
@Test

View File

@@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import static com.dedicatedcode.reitti.TestConstants.Points.*;
@@ -41,42 +42,41 @@ public class ProcessingPipelineTest {
testingService.importAndProcess(user, "/data/gpx/20250617.gpx");
List<ProcessedVisit> processedVisits = currentVisits();
assertEquals(5, processedVisits.size());
assertEquals(4, processedVisits.size());
assertVisit(processedVisits.get(0), "2025-06-16T22:00:09.154Z", "2025-06-17T05:40:26Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-17T05:43:37.962Z", "2025-06-17T05:55:03.792Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-17T05:58:10.797Z", "2025-06-17T13:08:53.346Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-17T13:12:01.542Z", "2025-06-17T13:18:51.590Z" , ST_THOMAS);
assertVisit(processedVisits.get(4), "2025-06-17T13:21:28.334Z", "2025-06-17T21:59:44.876Z" , MOLTKESTR);
assertVisit(processedVisits.get(0), "2025-06-16T22:00:09Z", "2025-06-17T05:40:26Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-17T05:58:10Z", "2025-06-17T13:08:53Z" , MOLTKESTR);
assertVisit(processedVisits.get(2), "2025-06-17T13:12:33Z", "2025-06-17T13:18:20Z" , ST_THOMAS);
assertVisit(processedVisits.get(3), "2025-06-17T13:21:28Z", "2025-06-17T21:59:44Z" , MOLTKESTR);
List<Trip> trips = currenTrips();
assertEquals(4, trips.size());
assertTrip(trips.get(0), "2025-06-17T05:40:26Z" , MOLTKESTR, "2025-06-17T05:43:37.962Z" , ST_THOMAS);
assertTrip(trips.get(1), "2025-06-17T05:55:03.792Z" , ST_THOMAS, "2025-06-17T05:58:10.797Z" , MOLTKESTR);
assertTrip(trips.get(2), "2025-06-17T13:08:53.346Z" , MOLTKESTR, "2025-06-17T13:12:01.542Z" , ST_THOMAS);
assertTrip(trips.get(3), "2025-06-17T13:18:51.590Z" , ST_THOMAS, "2025-06-17T13:21:28.334Z" , MOLTKESTR);
assertEquals(3, trips.size());
assertTrip(trips.get(0), "2025-06-17T05:40:26Z","2025-06-17T05:58:10Z", MOLTKESTR, MOLTKESTR);
assertTrip(trips.get(1), "2025-06-17T13:08:53Z","2025-06-17T13:12:33Z", MOLTKESTR, ST_THOMAS);
assertTrip(trips.get(2), "2025-06-17T13:18:20Z","2025-06-17T13:21:28Z", ST_THOMAS, MOLTKESTR);
testingService.importAndProcess(user, "/data/gpx/20250618.gpx");
processedVisits = currentVisits();
assertEquals(10, processedVisits.size());
assertEquals(11, processedVisits.size());
//should not touch visits before the new data
assertVisit(processedVisits.get(0), "2025-06-16T22:00:09.154Z", "2025-06-17T05:41:00Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-17T05:41:30.989Z", "2025-06-17T05:57:07.729Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-17T05:57:41Z" , "2025-06-17T13:09:29Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-17T13:09:51.476Z", "2025-06-17T13:20:24.494Z" , ST_THOMAS);
assertVisit(processedVisits.get(0), "2025-06-16T22:00:09Z", "2025-06-17T05:42:33Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-17T05:43:05Z", "2025-06-17T05:55:34Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-17T05:55:56Z", "2025-06-17T13:11:30Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-17T13:12:01Z", "2025-06-17T13:18:51Z" , ST_THOMAS);
//should extend the last visit of the old day
assertVisit(processedVisits.get(4), "2025-06-17T13:20:58Z" , "2025-06-18T05:46:43Z", MOLTKESTR);
assertVisit(processedVisits.get(4), "2025-06-17T13:21:28Z", "2025-06-18T05:45:36Z", MOLTKESTR);
//new visits
assertVisit(processedVisits.get(5), "2025-06-18T05:47:13.682Z" ,"2025-06-18T06:04:02.435Z" , ST_THOMAS);
assertVisit(processedVisits.get(6), "2025-06-18T06:04:36Z" ,"2025-06-18T13:01:57Z" , MOLTKESTR);
assertVisit(processedVisits.get(7), "2025-06-18T13:02:27.656Z" ,"2025-06-18T13:14:19.417Z" , ST_THOMAS);
assertVisit(processedVisits.get(8), "2025-06-18T13:33:05Z" ,"2025-06-18T15:50:40Z" , GARTEN);
assertVisit(processedVisits.get(9), "2025-06-18T16:02:38Z" ,"2025-06-18T21:59:29.055Z" , MOLTKESTR);
assertVisit(processedVisits.get(5) , "2025-06-18T05:46:10Z", "2025-06-18T05:53:01Z", MOLTKEPLATZ);
assertVisit(processedVisits.get(6) , "2025-06-18T05:54:37Z", "2025-06-18T06:02:05Z", ST_THOMAS);
assertVisit(processedVisits.get(7) , "2025-06-18T06:05:07Z", "2025-06-18T13:01:23Z", MOLTKESTR);
assertVisit(processedVisits.get(8) , "2025-06-18T13:05:04Z", "2025-06-18T13:13:47Z", ST_THOMAS);
assertVisit(processedVisits.get(9) , "2025-06-18T13:33:35Z", "2025-06-18T15:50:40Z", GARTEN);
assertVisit(processedVisits.get(10), "2025-06-18T16:03:09Z", "2025-06-18T21:59:29Z", MOLTKESTR);
}
@Test
@@ -86,12 +86,12 @@ public class ProcessingPipelineTest {
List<ProcessedVisit> processedVisits = currentVisits();
assertEquals(6, processedVisits.size());
assertVisit(processedVisits.get(0), "2025-06-17T22:00:15.843Z" ,"2025-06-18T05:46:10Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-18T05:53:33.667Z" ,"2025-06-18T06:01:54.440Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-18T06:04:36Z" ,"2025-06-18T13:01:57Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-18T13:04:33.424Z" ,"2025-06-18T13:13:47.443Z" , ST_THOMAS);
assertVisit(processedVisits.get(4), "2025-06-18T13:33:35.626Z" ,"2025-06-18T15:50:40Z" , GARTEN);
assertVisit(processedVisits.get(5), "2025-06-18T16:02:38Z" ,"2025-06-18T21:59:29.055Z" , MOLTKESTR);
assertVisit(processedVisits.get(0), "2025-06-17T22:00:15Z","2025-06-18T05:45:36Z", MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-18T05:54:37Z","2025-06-18T06:02:05Z", ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-18T06:05:07Z","2025-06-18T13:01:23Z", MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-18T13:05:04Z","2025-06-18T13:13:47Z", ST_THOMAS);
assertVisit(processedVisits.get(4), "2025-06-18T13:33:35Z","2025-06-18T15:50:40Z", GARTEN);
assertVisit(processedVisits.get(5), "2025-06-18T16:03:09Z","2025-06-18T21:59:29Z", MOLTKESTR);
testingService.importAndProcess(user, "/data/gpx/20250617.gpx");
@@ -100,20 +100,20 @@ public class ProcessingPipelineTest {
assertEquals(10, processedVisits.size());
//new visits
assertVisit(processedVisits.get(0), "2025-06-16T22:00:09.154Z", "2025-06-17T05:41:00Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-17T05:41:30.989Z", "2025-06-17T05:57:07.729Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-17T05:57:41Z" , "2025-06-17T13:09:29Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-17T13:09:51.476Z", "2025-06-17T13:20:24.494Z" , ST_THOMAS);
assertVisit(processedVisits.get(0),"2025-06-16T22:00:09Z", "2025-06-17T05:42:33Z", MOLTKESTR);
assertVisit(processedVisits.get(1),"2025-06-17T05:43:05Z", "2025-06-17T05:55:34Z", ST_THOMAS);
assertVisit(processedVisits.get(2),"2025-06-17T05:55:56Z", "2025-06-17T13:11:30Z", MOLTKESTR);
assertVisit(processedVisits.get(3),"2025-06-17T13:12:01Z", "2025-06-17T13:18:51Z", ST_THOMAS);
//should extend the last visit of the old day
assertVisit(processedVisits.get(4), "2025-06-17T13:20:58Z" , "2025-06-18T05:46:43Z" , MOLTKESTR);
assertVisit(processedVisits.get(4),"2025-06-17T13:19:22Z", "2025-06-18T05:53:01Z", MOLTKESTR);
//should not touch visits after the new data
assertVisit(processedVisits.get(5), "2025-06-18T05:47:13.682Z" ,"2025-06-18T06:04:02.435Z" , ST_THOMAS);
assertVisit(processedVisits.get(6), "2025-06-18T06:04:36Z" ,"2025-06-18T13:01:57Z" , MOLTKESTR);
assertVisit(processedVisits.get(7), "2025-06-18T13:02:27.656Z" ,"2025-06-18T13:14:19.417Z" , ST_THOMAS);
assertVisit(processedVisits.get(8), "2025-06-18T13:33:05Z" ,"2025-06-18T15:50:40Z" , GARTEN);
assertVisit(processedVisits.get(9), "2025-06-18T16:02:38Z" ,"2025-06-18T21:59:29.055Z" , MOLTKESTR);
assertVisit(processedVisits.get(5),"2025-06-18T05:53:33Z", "2025-06-18T06:02:05Z", ST_THOMAS);
assertVisit(processedVisits.get(6),"2025-06-18T06:03:00Z", "2025-06-18T13:04:01Z", MOLTKESTR);
assertVisit(processedVisits.get(7),"2025-06-18T13:04:33Z", "2025-06-18T13:13:47Z", ST_THOMAS);
assertVisit(processedVisits.get(8),"2025-06-18T13:33:35Z", "2025-06-18T15:50:40Z", GARTEN);
assertVisit(processedVisits.get(9),"2025-06-18T16:02:02Z", "2025-06-18T21:59:29Z", MOLTKESTR);
}
@Test
@@ -123,12 +123,12 @@ public class ProcessingPipelineTest {
List<ProcessedVisit> processedVisits = currentVisits();
assertEquals(6, processedVisits.size());
assertVisit(processedVisits.get(0), "2025-06-17T22:00:15.843Z" , "2025-06-18T05:46:10Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-18T05:53:33.667Z" ,"2025-06-18T06:01:54.440Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-18T06:04:36Z" ,"2025-06-18T13:01:57Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-18T13:04:33.424Z" ,"2025-06-18T13:13:47.443Z" , ST_THOMAS);
assertVisit(processedVisits.get(4), "2025-06-18T13:33:35.626Z" ,"2025-06-18T15:50:40Z" , GARTEN);
assertVisit(processedVisits.get(5), "2025-06-18T16:02:38Z" ,"2025-06-18T21:59:29.055Z" , MOLTKESTR);
assertVisit(processedVisits.get(0), "2025-06-17T22:00:15Z","2025-06-18T05:45:36Z" , MOLTKESTR);
assertVisit(processedVisits.get(1), "2025-06-18T05:54:37Z","2025-06-18T06:02:05Z" , ST_THOMAS);
assertVisit(processedVisits.get(2), "2025-06-18T06:05:07Z","2025-06-18T13:01:23Z" , MOLTKESTR);
assertVisit(processedVisits.get(3), "2025-06-18T13:05:04Z","2025-06-18T13:13:47Z" , ST_THOMAS);
assertVisit(processedVisits.get(4), "2025-06-18T13:33:35Z","2025-06-18T15:50:40Z" , GARTEN);
assertVisit(processedVisits.get(5), "2025-06-18T16:03:09Z","2025-06-18T21:59:29Z" , MOLTKESTR);
}
@@ -181,8 +181,8 @@ public class ProcessingPipelineTest {
}
private static void assertVisit(ProcessedVisit processedVisit, String startTime, String endTime, GeoPoint location) {
assertEquals(Instant.parse(startTime), processedVisit.getStartTime());
assertEquals(Instant.parse(endTime), processedVisit.getEndTime());
assertEquals(Instant.parse(startTime).truncatedTo(ChronoUnit.SECONDS), processedVisit.getStartTime().truncatedTo(ChronoUnit.SECONDS));
assertEquals(Instant.parse(endTime).truncatedTo(ChronoUnit.SECONDS), processedVisit.getEndTime().truncatedTo(ChronoUnit.SECONDS));
GeoPoint currentLocation = new GeoPoint(processedVisit.getPlace().getLatitudeCentroid(), processedVisit.getPlace().getLongitudeCentroid());
assertTrue(location.near(currentLocation), "Locations are not near to each other. \nExpected [" + currentLocation + "] to be in range \nto [" + location + "]");
}
@@ -195,9 +195,9 @@ public class ProcessingPipelineTest {
return this.tripJdbcService.findByUser(this.user);
}
private static void assertTrip(Trip trip, String startTime, GeoPoint startLocation, String endTime, GeoPoint endLocation) {
assertEquals(Instant.parse(startTime), trip.getStartTime());
assertEquals(Instant.parse(endTime), trip.getEndTime());
private static void assertTrip(Trip trip, String startTime, String endTime, GeoPoint startLocation, GeoPoint endLocation) {
assertEquals(Instant.parse(startTime).truncatedTo(ChronoUnit.SECONDS), trip.getStartTime().truncatedTo(ChronoUnit.SECONDS));
assertEquals(Instant.parse(endTime).truncatedTo(ChronoUnit.SECONDS), trip.getEndTime().truncatedTo(ChronoUnit.SECONDS));
GeoPoint actualStartLocation = GeoPoint.from(trip.getStartVisit().getPlace().getLatitudeCentroid(), trip.getStartVisit().getPlace().getLongitudeCentroid());
assertTrue(startLocation.near(actualStartLocation),

View File

@@ -76,8 +76,8 @@ class SyntheticLocationPointGeneratorTest {
// Verify middle point coordinates (should be halfway between start and end)
LocationPoint middlePoint = syntheticPoints.get(1); // 30 seconds = 50% of the way
assertEquals(50.001, middlePoint.getLatitude(), 0.0001);
assertEquals(8.001, middlePoint.getLongitude(), 0.0001);
assertEquals(50.0012, middlePoint.getLatitude(), 0.0001);
assertEquals(8.001, middlePoint.getLongitude(), 0.001);
}
@Test
@@ -100,8 +100,8 @@ class SyntheticLocationPointGeneratorTest {
// Then: Middle point should have interpolated values
LocationPoint middlePoint = syntheticPoints.get(1); // 30 seconds = 50% of the way
assertEquals(15.0, middlePoint.getAccuracyMeters(), 0.1); // 10 + (20-10) * 0.5
assertEquals(110.0, middlePoint.getElevationMeters(), 0.1); // 100 + (120-100) * 0.5
assertEquals(14.785, middlePoint.getAccuracyMeters(), 0.05); // 10 + (20-10) * 0.5
assertEquals(109.57, middlePoint.getElevationMeters(), 0.02); // 100 + (120-100) * 0.5
}
@Test