mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
Fixes for place polygons (#596)
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user