Fixes for place polygons (#596)

This commit is contained in:
Daniel Graf
2025-12-28 09:54:23 +01:00
committed by GitHub
parent 44a675d9c1
commit 5aafbd8e77
7 changed files with 136 additions and 10 deletions

View File

@@ -1,8 +1,10 @@
package com.dedicatedcode.reitti.model;
import com.dedicatedcode.reitti.model.geo.GeoPoint;
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
import java.time.ZoneId;
import java.util.List;
public record PlaceInformationOverride(String name, SignificantPlace.PlaceType category, ZoneId timezone) {
public record PlaceInformationOverride(String name, SignificantPlace.PlaceType category, ZoneId timezone, List<GeoPoint> polygon) {
}

View File

@@ -8,6 +8,7 @@ import com.dedicatedcode.reitti.model.security.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
@@ -23,12 +24,23 @@ public class SignificantPlaceOverrideJdbcService {
public Optional<PlaceInformationOverride> findByUserAndPoint(User user, GeoPoint point) {
double meterInDegrees = GeoUtils.metersToDegreesAtPosition(5.0, point.latitude());
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";
String sql = """
SELECT name, category, timezone, ST_AsText(polygon) as polygon FROM significant_places_overrides
WHERE user_id = ?
AND ST_DWithin(
COALESCE(polygon, ST_Buffer(geom, ?)),
ST_GeomFromText(?, '4326'),
0
)
ORDER BY ST_Distance(geom, ST_GeomFromText(?, '4326')) LIMIT 1
""";
String pointWkt = pointReaderWriter.write(point);
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));
ZoneId.of(rs.getString("timezone")),
pointReaderWriter.wktToPolygon(rs.getString("polygon"))
), user.getId(), meterInDegrees, pointWkt, pointWkt);
return override.stream().findFirst();
}
@@ -40,8 +52,11 @@ public class SignificantPlaceOverrideJdbcService {
GeoPoint point = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
double meterInDegrees = GeoUtils.metersToDegreesAtPosition(5.0, place.getLatitudeCentroid());
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());
String polygonWkt = this.pointReaderWriter.polygonToWkt(place.getPolygon());
String sql = "INSERT INTO significant_places_overrides (user_id, geom, name, category, timezone, polygon) VALUES (?, ST_GeomFromText(?, '4326'), ?, ?, ?, CASE WHEN ?::text IS NOT NULL THEN ST_GeomFromText(?, '4326') END)";
jdbcTemplate.update(sql, user.getId(), pointReaderWriter.write(point), place.getName(), place.getType().name(), place.getTimezone().getId(), polygonWkt, polygonWkt);
}
public void clear(User user, SignificantPlace place) {

View File

@@ -809,7 +809,8 @@ public class UnifiedLocationProcessingService {
significantPlace = significantPlace
.withName(override.get().name())
.withType(override.get().category())
.withTimezone(override.get().timezone());
.withTimezone(override.get().timezone())
.withPolygon(override.get().polygon());
}
significantPlace = previewId == null ? this.significantPlaceJdbcService.create(user, significantPlace) : this.previewSignificantPlaceJdbcService.create(user, previewId, significantPlace);
publishSignificantPlaceCreatedEvent(user, significantPlace, previewId, traceId);

View File

@@ -0,0 +1,4 @@
ALTER TABLE significant_places_overrides ADD COLUMN polygon GEOMETRY(POLYGON, 4326) DEFAULT NULL;
CREATE INDEX idx_significant_places_override_polygon ON significant_places_overrides USING GIST (polygon);

View File

@@ -216,7 +216,8 @@ class PolygonEditor {
}
savePolygon() {
if (!document.getElementById('save-btn').disabled) {
const saveBtn = document.getElementById('save-btn');
if (!saveBtn.disabled || saveBtn.classList.contains('btn-loading')) {
document.getElementById('polygon-form').submit();
}
}

View File

@@ -126,6 +126,34 @@
.drawer-content {
padding: 24px;
}
.btn-loading {
position: relative;
pointer-events: none;
}
.btn-loading .btn-text {
opacity: 0;
}
.btn-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<script src="/js/leaflet.js"></script>
<script src="/js/polygon-editor.js"></script>
@@ -211,7 +239,9 @@
<div class="separator"></div>
<div>
<button type="button" id="save-btn" class="btn btn-default btn-block" th:text="#{form.save}">Save</button>
<button type="button" id="save-btn" class="btn btn-default btn-block">
<span class="btn-text" th:text="#{form.save}">Save</span>
</button>
<a th:href="${returnUrl}" class="btn btn-default btn-block" th:text="#{form.cancel}">Cancel</a>
</div>
</div>
@@ -372,12 +402,27 @@
};
}
// Functions to manage button loading state
function showSaveLoading() {
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = true;
saveBtn.classList.add('btn-loading');
}
function hideSaveLoading() {
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = false;
saveBtn.classList.remove('btn-loading');
}
// Save button handler
document.getElementById('save-btn').addEventListener('click', function() {
checkBeforeSave();
});
function checkBeforeSave() {
showSaveLoading();
const formData = new FormData(document.getElementById('polygon-form'));
fetch(`/settings/places/${placeData.id}/check-update`, {
@@ -387,14 +432,17 @@
.then(response => response.json())
.then(data => {
if (data.canProceed && data.warnings.length === 0) {
// Keep loading state and proceed with save
polygonEditor.savePolygon();
} else {
// Hide loading state to show confirmation dialog
hideSaveLoading();
showConfirmationDialog(data.warnings);
}
})
.catch(error => {
console.error('Error checking update:', error);
// On error, proceed with save anyway
// Keep loading state and proceed with save anyway
polygonEditor.savePolygon();
});
}
@@ -404,6 +452,7 @@
const confirmMessage = /*[[#{places.update.confirmation.message}]]*/ 'The following changes will be made:\n\n{0}\n\nDo you want to continue?';
const message = confirmMessage.replace('{0}', warningsList);
if (confirm(message)) {
showSaveLoading();
polygonEditor.savePolygon();
}
}

View File

@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@@ -148,4 +149,57 @@ class SignificantPlaceOverrideJdbcServiceTest {
assertTrue(result2.isPresent());
assertEquals("Second Override", result2.get().name());
}
@Test
void testPolygonHandling() {
// Create a test user
User user = testingService.randomUser();
// Create a polygon for the place (a simple rectangle around the center point)
List<GeoPoint> polygon = List.of(
new GeoPoint(40.7120, -74.0070), // Southwest corner
new GeoPoint(40.7120, -74.0050), // Southeast corner
new GeoPoint(40.7136, -74.0050), // Northeast corner
new GeoPoint(40.7136, -74.0070), // Northwest corner
new GeoPoint(40.7120, -74.0070) // Close the polygon
);
// Create a SignificantPlace with a polygon
SignificantPlace place = new SignificantPlace(1L, "Polygon Place", "123 Polygon St", "Polygon City", "US", 40.7128, -74.0060, polygon, PlaceType.WORK, ZoneId.of("America/New_York"), false, 1L);
// Insert the override
significantPlaceOverrideJdbcService.insertOverride(user, place);
// Test finding by center point
GeoPoint centerPoint = new GeoPoint(place.getLatitudeCentroid(), place.getLongitudeCentroid());
Optional<PlaceInformationOverride> result = significantPlaceOverrideJdbcService.findByUserAndPoint(user, centerPoint);
assertTrue(result.isPresent());
assertEquals("Polygon Place", result.get().name());
assertEquals(PlaceType.WORK, result.get().category());
assertEquals(ZoneId.of("America/New_York"), result.get().timezone());
// Verify the polygon is returned correctly
assertNotNull(result.get().polygon());
assertEquals(5, result.get().polygon().size()); // Should have 5 points (including closing point)
// Verify the polygon points match what we inserted
List<GeoPoint> returnedPolygon = result.get().polygon();
for (int i = 0; i < polygon.size(); i++) {
assertEquals(polygon.get(i).latitude(), returnedPolygon.get(i).latitude(), 0.0001);
assertEquals(polygon.get(i).longitude(), returnedPolygon.get(i).longitude(), 0.0001);
}
// Test finding by a point inside the polygon
GeoPoint insidePoint = new GeoPoint(40.7128, -74.0060); // Should be inside the polygon
Optional<PlaceInformationOverride> resultInside = significantPlaceOverrideJdbcService.findByUserAndPoint(user, insidePoint);
assertTrue(resultInside.isPresent());
assertEquals("Polygon Place", resultInside.get().name());
// Test finding by a point outside the polygon but within 5m of center should still find it
GeoPoint nearbyPoint = new GeoPoint(40.71281, -74.006056); // Close to center but outside polygon
Optional<PlaceInformationOverride> resultNearby = significantPlaceOverrideJdbcService.findByUserAndPoint(user, nearbyPoint);
assertTrue(resultNearby.isPresent());
assertEquals("Polygon Place", resultNearby.get().name());
}
}