392 feature request improve home detection (#414)

This commit is contained in:
Daniel Graf
2025-11-06 14:44:40 +01:00
committed by GitHub
parent 6c84a75f5a
commit 030ecdeed6
11 changed files with 274 additions and 6 deletions

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version>
<version>3.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dedicatedcode</groupId>

View File

@@ -31,6 +31,7 @@ import java.util.*;
public class TimelineController {
private final SignificantPlaceJdbcService placeService;
private final SignificantPlaceOverrideJdbcService placeOverrideJdbcService;
private final UserJdbcService userJdbcService;
private final AvatarService avatarService;
@@ -42,7 +43,7 @@ public class TimelineController {
private final TripJdbcService tripJdbcService;
@Autowired
public TimelineController(SignificantPlaceJdbcService placeService,
public TimelineController(SignificantPlaceJdbcService placeService, SignificantPlaceOverrideJdbcService placeOverrideJdbcService,
UserJdbcService userJdbcService,
AvatarService avatarService,
ReittiIntegrationService reittiIntegrationService, UserSharingJdbcService userSharingJdbcService,
@@ -51,6 +52,7 @@ public class TimelineController {
TransportModeService transportModeService,
TripJdbcService tripJdbcService) {
this.placeService = placeService;
this.placeOverrideJdbcService = placeOverrideJdbcService;
this.userJdbcService = userJdbcService;
this.avatarService = avatarService;
this.reittiIntegrationService = reittiIntegrationService;
@@ -102,7 +104,9 @@ public class TimelineController {
}
}
User user = this.userJdbcService.findByUsername(principal.getName()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"));
placeService.update(updatedPlace);
placeOverrideJdbcService.insertOverride(user, updatedPlace);
// If we have timeline context, reload the entire timeline with the edited place selected
if (date != null) {

View File

@@ -8,6 +8,7 @@ import com.dedicatedcode.reitti.model.geocoding.RemoteGeocodeService;
import com.dedicatedcode.reitti.model.security.User;
import com.dedicatedcode.reitti.repository.GeocodeServiceJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceOverrideJdbcService;
import com.dedicatedcode.reitti.repository.UserJdbcService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
@@ -28,6 +29,7 @@ public class GeoCodingSettingsController {
private final GeocodeServiceJdbcService geocodeServiceJdbcService;
private final SignificantPlaceJdbcService placeJdbcService;
private final SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService;
private final UserJdbcService userJdbcService;
private final RabbitTemplate rabbitTemplate;
private final MessageSource messageSource;
@@ -37,6 +39,7 @@ public class GeoCodingSettingsController {
public GeoCodingSettingsController(GeocodeServiceJdbcService geocodeServiceJdbcService,
SignificantPlaceJdbcService placeJdbcService,
SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService,
UserJdbcService userJdbcService,
RabbitTemplate rabbitTemplate,
MessageSource messageSource,
@@ -44,6 +47,7 @@ public class GeoCodingSettingsController {
@Value("${reitti.geocoding.max-errors}") int maxErrors) {
this.geocodeServiceJdbcService = geocodeServiceJdbcService;
this.placeJdbcService = placeJdbcService;
this.significantPlaceOverrideJdbcService = significantPlaceOverrideJdbcService;
this.userJdbcService = userJdbcService;
this.rabbitTemplate = rabbitTemplate;
this.messageSource = messageSource;
@@ -168,6 +172,7 @@ public class GeoCodingSettingsController {
// Clear geocoding data for all places
for (SignificantPlace place : allPlaces) {
SignificantPlace clearedPlace = place.withGeocoded(false).withAddress(null);
this.significantPlaceOverrideJdbcService.clear(currentUser, clearedPlace);
placeJdbcService.update(clearedPlace);
}

View File

@@ -11,6 +11,7 @@ import com.dedicatedcode.reitti.model.geocoding.GeocodingResponse;
import com.dedicatedcode.reitti.model.security.User;
import com.dedicatedcode.reitti.repository.GeocodingResponseJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceOverrideJdbcService;
import com.dedicatedcode.reitti.service.PlaceService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
@@ -32,15 +33,22 @@ import java.util.stream.Collectors;
public class PlacesSettingsController {
private final PlaceService placeService;
private final SignificantPlaceJdbcService placeJdbcService;
private final SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService;
private final GeocodingResponseJdbcService geocodingResponseJdbcService;
private final RabbitTemplate rabbitTemplate;
private final MessageSource messageSource;
private final boolean dataManagementEnabled;
public PlacesSettingsController(PlaceService placeService, SignificantPlaceJdbcService placeJdbcService, GeocodingResponseJdbcService geocodingResponseJdbcService, RabbitTemplate rabbitTemplate, MessageSource messageSource,
public PlacesSettingsController(PlaceService placeService,
SignificantPlaceJdbcService placeJdbcService,
SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService,
GeocodingResponseJdbcService geocodingResponseJdbcService,
RabbitTemplate rabbitTemplate,
MessageSource messageSource,
@Value("${reitti.data-management.enabled:false}") boolean dataManagementEnabled) {
this.placeService = placeService;
this.placeJdbcService = placeJdbcService;
this.significantPlaceOverrideJdbcService = significantPlaceOverrideJdbcService;
this.geocodingResponseJdbcService = geocodingResponseJdbcService;
this.rabbitTemplate = rabbitTemplate;
this.messageSource = messageSource;
@@ -154,6 +162,7 @@ public class PlacesSettingsController {
}
placeJdbcService.update(updatedPlace);
significantPlaceOverrideJdbcService.insertOverride(user, updatedPlace);
model.addAttribute("successMessage", getMessage("message.success.place.updated"));
return editPlace(placeId, page, authentication, model);
} catch (Exception e) {
@@ -179,7 +188,7 @@ public class PlacesSettingsController {
// Clear geocoding data and mark as not geocoded
SignificantPlace clearedPlace = significantPlace.withGeocoded(false).withAddress(null);
placeJdbcService.update(clearedPlace);
significantPlaceOverrideJdbcService.clear(user, clearedPlace);
// Send SignificantPlaceCreatedEvent to trigger geocoding
SignificantPlaceCreatedEvent event = new SignificantPlaceCreatedEvent(
user.getUsername(),

View File

@@ -0,0 +1,8 @@
package com.dedicatedcode.reitti.model;
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
import java.time.ZoneId;
public record PlaceInformationOverride(String name, SignificantPlace.PlaceType category, ZoneId timezone) {
}

View File

@@ -1,5 +1,6 @@
package com.dedicatedcode.reitti.repository;
import com.dedicatedcode.reitti.model.geo.GeoPoint;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
@@ -29,4 +30,8 @@ public class PointReaderWriter {
public String write(double x, double y) {
return geometryFactory.createPoint(new Coordinate(x, y)).toString();
}
public String write(GeoPoint point) {
return write(point.longitude(), point.latitude());
}
}

View File

@@ -0,0 +1,51 @@
package com.dedicatedcode.reitti.repository;
import com.dedicatedcode.reitti.model.PlaceInformationOverride;
import com.dedicatedcode.reitti.model.geo.GeoPoint;
import com.dedicatedcode.reitti.model.geo.GeoUtils;
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
import com.dedicatedcode.reitti.model.security.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class SignificantPlaceOverrideJdbcService {
private final JdbcTemplate jdbcTemplate;
private final PointReaderWriter pointReaderWriter;
public SignificantPlaceOverrideJdbcService(JdbcTemplate jdbcTemplate, PointReaderWriter pointReaderWriter) {
this.jdbcTemplate = jdbcTemplate;
this.pointReaderWriter = pointReaderWriter;
}
public Optional<PlaceInformationOverride> findByUserAndPoint(User user, GeoPoint point) {
double meterInDegrees = GeoUtils.metersToDegreesAtPosition(5.0, point.latitude())[0];
String sql = "SELECT name, category, timezone FROM significant_places_overrides WHERE user_id = ? AND ST_DWithin(geom, ST_GeomFromText(?, '4326'), ?) ORDER BY ST_Distance(geom, ST_GeomFromText(?, '4326')) ASC LIMIT 1";
List<PlaceInformationOverride> override = jdbcTemplate.query(sql, (rs, rowNum) -> new PlaceInformationOverride(
rs.getString("name"),
SignificantPlace.PlaceType.valueOf(rs.getString("category")),
java.time.ZoneId.of(rs.getString("timezone"))
), user.getId(), pointReaderWriter.write(point), meterInDegrees, pointReaderWriter.write(point));
return override.stream().findFirst();
}
public Optional<PlaceInformationOverride> findByUserAndPoint(User user, SignificantPlace place) {
return findByUserAndPoint(user, new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid()));
}
public void insertOverride(User user, SignificantPlace place) {
GeoPoint point = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
double meterInDegrees = GeoUtils.metersToDegreesAtPosition(5.0, place.getLatitudeCentroid())[0];
this.jdbcTemplate.update("DELETE FROM significant_places_overrides WHERE user_id = ? AND ST_DWithin(geom, ST_GeomFromText(?, '4326'), ?)", user.getId(), pointReaderWriter.write(point), meterInDegrees);
String sql = "INSERT INTO significant_places_overrides (user_id, geom, name, category, timezone) VALUES (?, ST_GeomFromText(?, '4326'), ?, ?, ?)";
jdbcTemplate.update(sql, user.getId(), pointReaderWriter.write(point), place.getName(), place.getType().name(), place.getTimezone().getId());
}
public void clear(User user, SignificantPlace place) {
GeoPoint point = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
this.jdbcTemplate.update("DELETE FROM significant_places_overrides WHERE user_id = ? AND ST_Equals(geom, ST_GeomFromText(?, '4326'))", user.getId(), pointReaderWriter.write(point));
}
}

View File

@@ -1,10 +1,13 @@
package com.dedicatedcode.reitti.service.geocoding;
import com.dedicatedcode.reitti.event.SignificantPlaceCreatedEvent;
import com.dedicatedcode.reitti.model.PlaceInformationOverride;
import com.dedicatedcode.reitti.model.geo.GeoPoint;
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
import com.dedicatedcode.reitti.model.security.User;
import com.dedicatedcode.reitti.repository.PreviewSignificantPlaceJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceJdbcService;
import com.dedicatedcode.reitti.repository.SignificantPlaceOverrideJdbcService;
import com.dedicatedcode.reitti.repository.UserJdbcService;
import com.dedicatedcode.reitti.service.UserNotificationService;
import org.slf4j.Logger;
@@ -21,17 +24,19 @@ public class ReverseGeocodingListener {
private final SignificantPlaceJdbcService significantPlaceJdbcService;
private final PreviewSignificantPlaceJdbcService previewSignificantPlaceJdbcService;
private final GeocodeServiceManager geocodeServiceManager;
private final SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService;
private final UserNotificationService userNotificationService;
private final UserJdbcService userJdbcService;
@Autowired
public ReverseGeocodingListener(SignificantPlaceJdbcService significantPlaceJdbcService,
PreviewSignificantPlaceJdbcService previewSignificantPlaceJdbcService,
GeocodeServiceManager geocodeServiceManager,
GeocodeServiceManager geocodeServiceManager, SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService,
UserNotificationService userNotificationService, UserJdbcService userJdbcService) {
this.significantPlaceJdbcService = significantPlaceJdbcService;
this.previewSignificantPlaceJdbcService = previewSignificantPlaceJdbcService;
this.geocodeServiceManager = geocodeServiceManager;
this.significantPlaceOverrideJdbcService = significantPlaceOverrideJdbcService;
this.userNotificationService = userNotificationService;
this.userJdbcService = userJdbcService;
}
@@ -72,7 +77,11 @@ public class ReverseGeocodingListener {
.withCity(city)
.withCountryCode(countryCode);
Optional<PlaceInformationOverride> override = this.significantPlaceOverrideJdbcService.findByUserAndPoint(user, place);
if (override.isPresent()) {
logger.info("Found override for place ID: {} with name: {}, type: {}, timezone: {}", place.getId(), override.get().name(), override.get().category(), override.get().timezone());
place = place.withName(override.get().name()).withType(override.get().category()).withTimezone(override.get().timezone());
}
if (event.previewId() == null) {
significantPlaceJdbcService.update(place.withGeocoded(true));
userNotificationService.placeUpdate(user, place);

View File

@@ -4,6 +4,7 @@ import com.dedicatedcode.reitti.config.RabbitMQConfig;
import com.dedicatedcode.reitti.event.ProcessedVisitCreatedEvent;
import com.dedicatedcode.reitti.event.SignificantPlaceCreatedEvent;
import com.dedicatedcode.reitti.event.VisitUpdatedEvent;
import com.dedicatedcode.reitti.model.PlaceInformationOverride;
import com.dedicatedcode.reitti.model.geo.*;
import com.dedicatedcode.reitti.model.processing.DetectionParameter;
import com.dedicatedcode.reitti.model.security.User;
@@ -43,6 +44,7 @@ public class VisitMergingService {
private final PreviewSignificantPlaceJdbcService previewSignificantPlaceJdbcService;
private final RawLocationPointJdbcService rawLocationPointJdbcService;
private final PreviewRawLocationPointJdbcService previewRawLocationPointJdbcService;
private final SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService;
private final GeometryFactory geometryFactory;
private final RabbitTemplate rabbitTemplate;
private final UserNotificationService userNotificationService;
@@ -60,6 +62,7 @@ public class VisitMergingService {
PreviewSignificantPlaceJdbcService previewSignificantPlaceJdbcService,
RawLocationPointJdbcService rawLocationPointJdbcService,
PreviewRawLocationPointJdbcService previewRawLocationPointJdbcService,
SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService,
GeometryFactory geometryFactory,
UserNotificationService userNotificationService,
GeoLocationTimezoneService timezoneService,
@@ -74,6 +77,7 @@ public class VisitMergingService {
this.previewSignificantPlaceJdbcService = previewSignificantPlaceJdbcService;
this.rawLocationPointJdbcService = rawLocationPointJdbcService;
this.previewRawLocationPointJdbcService = previewRawLocationPointJdbcService;
this.significantPlaceOverrideJdbcService = significantPlaceOverrideJdbcService;
this.geometryFactory = geometryFactory;
this.userNotificationService = userNotificationService;
this.timezoneService = timezoneService;
@@ -272,6 +276,16 @@ public class VisitMergingService {
if (timezone.isPresent()) {
significantPlace = significantPlace.withTimezone(timezone.get());
}
// Check for override
GeoPoint point = new GeoPoint(significantPlace.getLatitudeCentroid(), significantPlace.getLongitudeCentroid());
Optional<PlaceInformationOverride> override = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
if (override.isPresent()) {
logger.info("Found override for user [{}] and location [{}], using override information: {}", user.getUsername(), point, override.get());
significantPlace = significantPlace
.withName(override.get().name())
.withType(override.get().category())
.withTimezone(override.get().timezone());
}
significantPlace = previewId == null ? this.significantPlaceJdbcService.create(user, significantPlace) : this.previewSignificantPlaceJdbcService.create(user, previewId, significantPlace);
publishSignificantPlaceCreatedEvent(user, significantPlace, previewId);
return significantPlace;

View File

@@ -0,0 +1,11 @@
CREATE TABLE significant_places_overrides
(
user_id BIGINT REFERENCES users (id),
geom geometry(POINT, 4326),
name VARCHAR(255),
category VARCHAR(255),
timezone VARCHAR(255),
CONSTRAINT unique_user_geom UNIQUE (user_id, geom)
);
INSERT INTO significant_places_overrides SELECT user_id, geom, name, type, timezone FROM significant_places ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,152 @@
package com.dedicatedcode.reitti.repository;
import com.dedicatedcode.reitti.IntegrationTest;
import com.dedicatedcode.reitti.model.PlaceInformationOverride;
import com.dedicatedcode.reitti.model.geo.GeoPoint;
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
import com.dedicatedcode.reitti.model.geo.SignificantPlace.PlaceType;
import com.dedicatedcode.reitti.model.security.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.ZoneId;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@IntegrationTest
class SignificantPlaceOverrideJdbcServiceTest {
@Autowired
private SignificantPlaceOverrideJdbcService significantPlaceOverrideJdbcService;
@Test
void testFindByUserAndPoint_ExistingOverride() {
// Create a test user (assuming a user exists or create one; for simplicity, assume user ID 1 exists)
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Create a GeoPoint
GeoPoint point = new GeoPoint(40.7128, -74.0060); // Example: New York coordinates
// Create a SignificantPlace with the override details
SignificantPlace place = new SignificantPlace(1L, "Home Override", "123 Main St", "New York", "US", 40.7128, -74.0060, PlaceType.HOME, ZoneId.of("America/New_York"), false, 1L);
// Insert the override using the service
significantPlaceOverrideJdbcService.insertOverride(user, place);
// Now test the find method
Optional<PlaceInformationOverride> result = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
assertTrue(result.isPresent());
assertEquals("Home Override", result.get().name());
assertEquals(PlaceType.HOME, result.get().category());
assertEquals(ZoneId.of("America/New_York"), result.get().timezone());
}
@Test
void testFindByUserAndPoint_NoOverride() {
// Create a test user
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Create a GeoPoint that doesn't have an override
GeoPoint point = new GeoPoint(51.5074, -0.1278); // Example: London coordinates
// Test the find method
Optional<PlaceInformationOverride> result = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
assertFalse(result.isPresent());
}
@Test
void testInsertOverride() {
// Create a test user
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Create a SignificantPlace
SignificantPlace place = new SignificantPlace(1L, "Test Place", "123 Test St", "Test City", "US", 40.7128, -74.0060, PlaceType.HOME, ZoneId.of("America/New_York"), false, 1L);
// Insert the override
significantPlaceOverrideJdbcService.insertOverride(user, place);
// Verify by finding it
GeoPoint point = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
Optional<PlaceInformationOverride> result = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
assertTrue(result.isPresent());
assertEquals("Test Place", result.get().name());
assertEquals(PlaceType.HOME, result.get().category());
assertEquals(ZoneId.of("America/New_York"), result.get().timezone());
}
@Test
void testClearOverride() {
// Create a test user
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Create a SignificantPlace
SignificantPlace place = new SignificantPlace(1L, "Test Place", "123 Test St", "Test City", "US", 40.7128, -74.0060, PlaceType.HOME, ZoneId.of("America/New_York"), false, 1L);
// Insert the override
significantPlaceOverrideJdbcService.insertOverride(user, place);
// Verify it exists
GeoPoint point = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
Optional<PlaceInformationOverride> resultBeforeClear = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
assertTrue(resultBeforeClear.isPresent());
// Clear the override
significantPlaceOverrideJdbcService.clear(user, place);
// Verify it no longer exists
Optional<PlaceInformationOverride> resultAfterClear = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point);
assertFalse(resultAfterClear.isPresent());
}
@Test
void testFindByUserAndPoint_Within5mRadius() {
// Create a test user
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Create a SignificantPlace at a specific location
SignificantPlace place = new SignificantPlace(1L, "Nearby Override", "456 Nearby St", "Nearby City", "US", 40.7128, -74.0060, PlaceType.WORK, ZoneId.of("America/New_York"), false, 1L);
significantPlaceOverrideJdbcService.insertOverride(user, place);
// Create a GeoPoint very close (within 5m) to the place
// Approximate 5m at this latitude: ~0.000045 degrees latitude, ~0.000056 degrees longitude
GeoPoint closePoint = new GeoPoint(40.712845, -74.006056); // Approximately 5m away
// Test that the override is found from the close point
Optional<PlaceInformationOverride> result = significantPlaceOverrideJdbcService.findByUserAndPoint(user, closePoint);
assertTrue(result.isPresent());
assertEquals("Nearby Override", result.get().name());
}
@Test
void testInsertOverride_DropsNearbyOverrides() {
// Create a test user
User user = new User(1L, "testuser", "password", "Test User", null, null, null, 1L);
// Insert first override
SignificantPlace place1 = new SignificantPlace(1L, "First Override", "123 First St", "First City", "US", 40.7128, -74.0060, PlaceType.HOME, ZoneId.of("America/New_York"), false, 1L);
significantPlaceOverrideJdbcService.insertOverride(user, place1);
// Verify first override exists
GeoPoint point1 = new GeoPoint(place1.getLatitudeCentroid(), place1.getLongitudeCentroid());
Optional<PlaceInformationOverride> result1 = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point1);
assertTrue(result1.isPresent());
// Insert second override very close (within 5m)
SignificantPlace place2 = new SignificantPlace(2L, "Second Override", "456 Second St", "Second City", "US", 40.712845, -74.0060, PlaceType.WORK, ZoneId.of("America/New_York"), false, 1L);
significantPlaceOverrideJdbcService.insertOverride(user, place2);
// Verify first override is dropped (since it's within 5m of the new one)
Optional<PlaceInformationOverride> result1AfterInsert = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point1);
assertFalse(result1AfterInsert.isPresent());
// Verify second override exists
GeoPoint point2 = new GeoPoint(place2.getLatitudeCentroid(), place2.getLongitudeCentroid());
Optional<PlaceInformationOverride> result2 = significantPlaceOverrideJdbcService.findByUserAndPoint(user, point2);
assertTrue(result2.isPresent());
assertEquals("Second Override", result2.get().name());
}
}