diff --git a/README.md b/README.md index b6045c0f..eea2d18f 100644 --- a/README.md +++ b/README.md @@ -205,10 +205,11 @@ The included `docker-compose.yml` provides a complete setup with: | `REDIS_PORT` | Redis port | 6379 | 6379 | | `REDIS_USERNAME` | Redis username (optional) | | username | | `REDIS_PASSWORD` | Redis password (optional) | | password | +| `ADVERTISE_URI` | Routable URL of the instance. Used for federation of multiple instances. (optional) | | https://reitti.lab | | `OIDC_ENABLED` | Whether to enable OIDC sign-ins | false | true | | `OIDC_CLIENT_ID` | Your OpenID Connect Client ID (from your provider) | | google | | `OIDC_CLIENT_SECRET` | Your OpenID Connect Client secret (from your provider) | | F0oxfg8b2rp5X97YPS92C2ERxof1oike | -| `OIDC_ISSUER_URI` | Your OpenID Connect Provider Discovery URI (don't include the /.well-known/openid-configuration part of the URI) | | https://github.com/login/oauth | +| `OIDC_ISSUER_URI` | Your OpenID Connect Provider Discovery URI (don't include the /.well-known/openid-configuration part of the URI) | | https://github.com/login/oauth | | `OIDC_SCOPE` | Your OpenID Connect scopes for your user (optional) | openid,profile | openid,profile | | `PHOTON_BASE_URL` | Base URL for Photon geocoding service | | | | `PROCESSING_WAIT_TIME` | How many seconds to wait after the last data input before starting to process all unprocessed data. (⚠️ This needs to be lower than your integrated app reports data in Reitti) | 15 | 15 | diff --git a/docs/tools/gpx-sender/README.md b/docs/tools/gpx-sender/README.md new file mode 100644 index 00000000..4babdcdf --- /dev/null +++ b/docs/tools/gpx-sender/README.md @@ -0,0 +1,42 @@ +# GPX Sender Tool + +A CLI utility to send GPX track data to a Reitti instance via the Owntracks ingest API. + +## Building + +```bash +cd docs/tools/gpx-sender +mvn clean package +``` + +## Usage + +```bash +java -jar target/gpx-sender-1.0.0.jar --url --token [--interval ] +``` + +### Parameters + +- `gpx-file`: Path to the GPX file containing track points (positional parameter) +- `--url`: Base URL of the Reitti instance (e.g., `http://localhost:8080`) +- `--token`: API token for authentication +- `--interval`: Optional interval between sending points (default: 15 seconds) + +### Example + +```bash +java -jar target/gpx-sender-1.0.0.jar my-track.gpx --url http://localhost:8080 --token your-api-token --interval 10 +``` + +## How it works + +1. Parses the GPX file to extract track points with coordinates and timestamps +2. Starts sending from the current time, with each subsequent point sent at the specified interval +3. Converts each point to Owntracks format and sends via HTTP POST to `/api/v1/ingest/owntracks` +4. Waits the specified interval between each point transmission + +## Requirements + +- Java 17 or higher +- Valid API token for the Reitti instance +- GPX file with track points diff --git a/docs/tools/gpx-sender/pom.xml b/docs/tools/gpx-sender/pom.xml new file mode 100644 index 00000000..6be5b141 --- /dev/null +++ b/docs/tools/gpx-sender/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.dedicatedcode.reitti.tools + gpx-sender + 1.0.0 + jar + + + 17 + 17 + UTF-8 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.2 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + com.dedicatedcode.reitti.tools.GpxSender + + + + + + + + + diff --git a/docs/tools/gpx-sender/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java b/docs/tools/gpx-sender/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java new file mode 100644 index 00000000..6753a5f6 --- /dev/null +++ b/docs/tools/gpx-sender/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java @@ -0,0 +1,209 @@ +package com.dedicatedcode.reitti.tools; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public class GpxSender { + + private static class TrackPoint { + public final double latitude; + public final double longitude; + public final Instant timestamp; + + public TrackPoint(double latitude, double longitude, Instant timestamp) { + this.latitude = latitude; + this.longitude = longitude; + this.timestamp = timestamp; + } + } + + private static class OwntracksMessage { + public String _type = "location"; + public double acc; + public double lat; + public double lon; + public long tst; + + public OwntracksMessage(double lat, double lon, long tst, double acc) { + this.lat = lat; + this.lon = lon; + this.tst = tst; + } + } + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Usage: java -jar gpx-sender.jar --url --token [--interval ]"); + System.err.println("Example: java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --interval 15"); + System.exit(1); + } + + String gpxFile = args[0]; + String reittiUrl = null; + String apiToken = null; + int intervalSeconds = 15; + + // Parse named parameters + for (int i = 1; i < args.length; i++) { + switch (args[i]) { + case "--url": + if (i + 1 < args.length) { + reittiUrl = args[++i]; + } + break; + case "--token": + if (i + 1 < args.length) { + apiToken = args[++i]; + } + break; + case "--interval": + if (i + 1 < args.length) { + intervalSeconds = Integer.parseInt(args[++i]); + } + break; + default: + System.err.println("Unknown parameter: " + args[i]); + System.exit(1); + } + } + + if (reittiUrl == null || apiToken == null) { + System.err.println("Both --url and --token parameters are required"); + System.exit(1); + } + + try { + List trackPoints = parseGpxFile(gpxFile); + if (trackPoints.isEmpty()) { + System.err.println("No track points found in GPX file"); + System.exit(1); + } + + System.out.println("Loaded " + trackPoints.size() + " track points from " + gpxFile); + System.out.println("Sending to: " + reittiUrl); + System.out.println("Interval: " + intervalSeconds + " seconds"); + + sendTrackPoints(trackPoints, reittiUrl, apiToken, intervalSeconds); + + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static List parseGpxFile(String gpxFile) throws Exception { + List trackPoints = new ArrayList<>(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new File(gpxFile)); + + NodeList trkptNodes = document.getElementsByTagName("trkpt"); + + for (int i = 0; i < trkptNodes.getLength(); i++) { + Element trkpt = (Element) trkptNodes.item(i); + + double lat = Double.parseDouble(trkpt.getAttribute("lat")); + double lon = Double.parseDouble(trkpt.getAttribute("lon")); + + NodeList timeNodes = trkpt.getElementsByTagName("time"); + Instant timestamp = null; + if (timeNodes.getLength() > 0) { + String timeStr = timeNodes.item(0).getTextContent(); + timestamp = Instant.parse(timeStr); + } + + trackPoints.add(new TrackPoint(lat, lon, timestamp)); + } + + return trackPoints; + } + + private static void sendTrackPoints(List trackPoints, String reittiUrl, String apiToken, int intervalSeconds) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + // Start from current time and send points with their original intervals + Instant startTime = Instant.now(); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + for (int i = 0; i < trackPoints.size(); i++) { + TrackPoint point = trackPoints.get(i); + + // Calculate adjusted timestamp - start from now and preserve original intervals + Instant adjustedTime; + if (i == 0) { + // First point gets current time + adjustedTime = startTime; + } else if (i > 0 && trackPoints.get(i-1).timestamp != null && point.timestamp != null) { + // Calculate time difference from previous point and add to previous adjusted time + TrackPoint prevPoint = trackPoints.get(i-1); + long durationFromPrev = point.timestamp.getEpochSecond() - prevPoint.timestamp.getEpochSecond(); + adjustedTime = startTime.plusSeconds((long) i * intervalSeconds); + } else { + // Fallback: distribute points evenly from start time + adjustedTime = startTime.plusSeconds((long) i * intervalSeconds); + } + + // Create Owntracks message + OwntracksMessage message = new OwntracksMessage( + point.latitude, + point.longitude, + adjustedTime.getEpochSecond(), + 10.0 + ); + + // Send HTTP request + String url = reittiUrl + "/api/v1/ingest/owntracks"; + HttpPost post = new HttpPost(url); + post.setHeader("Authorization", "Bearer " + apiToken); + post.setHeader("Content-Type", "application/json"); + + String json = objectMapper.writeValueAsString(message); + post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON)); + + System.out.printf("Sending point %d/%d: lat=%.6f, lon=%.6f, time=%s%n", + i + 1, trackPoints.size(), point.latitude, point.longitude, + adjustedTime.toString()); + + try { + httpClient.execute(post, response -> { + int statusCode = response.getCode(); + if (statusCode >= 200 && statusCode < 300) { + System.out.println("✓ Sent successfully"); + } else { + System.err.println("✗ Failed with status: " + statusCode); + } + return null; + }); + } catch (Exception e) { + System.err.println("✗ Error sending point: " + e.getMessage()); + } + + // Wait before sending next point (except for the last one) + if (i < trackPoints.size() - 1) { + Thread.sleep(intervalSeconds * 1000L); + } + } + } + + System.out.println("Finished sending all track points"); + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/config/AppConfig.java b/src/main/java/com/dedicatedcode/reitti/config/AppConfig.java index d2d6b70a..079e6f03 100644 --- a/src/main/java/com/dedicatedcode/reitti/config/AppConfig.java +++ b/src/main/java/com/dedicatedcode/reitti/config/AppConfig.java @@ -16,7 +16,6 @@ public class AppConfig { return new GeometryFactory(new PrecisionModel(), 4326); } - @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/com/dedicatedcode/reitti/config/SecurityConfig.java b/src/main/java/com/dedicatedcode/reitti/config/SecurityConfig.java index 7f588b0e..613872b1 100644 --- a/src/main/java/com/dedicatedcode/reitti/config/SecurityConfig.java +++ b/src/main/java/com/dedicatedcode/reitti/config/SecurityConfig.java @@ -34,6 +34,7 @@ public class SecurityConfig { .requestMatchers("/login").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/actuator/health").permitAll() + .requestMatchers("/api/v1/reitti-integration/notify/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(bearerTokenAuthFilter, AuthorizationFilter.class) diff --git a/src/main/java/com/dedicatedcode/reitti/controller/SseController.java b/src/main/java/com/dedicatedcode/reitti/controller/SseController.java index 3ddd02d8..033a993a 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/SseController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/SseController.java @@ -2,11 +2,11 @@ package com.dedicatedcode.reitti.controller; import com.dedicatedcode.reitti.model.User; import com.dedicatedcode.reitti.service.UserSseEmitterService; +import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -15,20 +15,19 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; public class SseController { private static final Logger log = LoggerFactory.getLogger(SseController.class); private final UserSseEmitterService emitterService; + private final ReittiIntegrationService reittiIntegrationService; - public SseController(UserSseEmitterService userSseEmitterService) { + public SseController(UserSseEmitterService userSseEmitterService, + ReittiIntegrationService reittiIntegrationService) { this.emitterService = userSseEmitterService; + this.reittiIntegrationService = reittiIntegrationService; } @GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter handleSseForUser(@AuthenticationPrincipal UserDetails userDetails) { - if (userDetails == null) { - throw new IllegalStateException("User not authenticated for SSE endpoint."); - } - - User user = (User) userDetails; - SseEmitter emitter = emitterService.addEmitter(user.getId()); + public SseEmitter handleSseForUser(@AuthenticationPrincipal User user) { + SseEmitter emitter = emitterService.addEmitter(user); + reittiIntegrationService.registerSubscriptionsForUser(user); log.info("New SSE connection from user: [{}]", user.getId()); return emitter; } -} \ No newline at end of file +} diff --git a/src/main/java/com/dedicatedcode/reitti/controller/api/ReittiIntegrationApiController.java b/src/main/java/com/dedicatedcode/reitti/controller/api/ReittiIntegrationApiController.java index 4a06e02d..85bd035d 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/api/ReittiIntegrationApiController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/api/ReittiIntegrationApiController.java @@ -1,33 +1,56 @@ package com.dedicatedcode.reitti.controller.api; import com.dedicatedcode.reitti.dto.ReittiRemoteInfo; +import com.dedicatedcode.reitti.dto.SubscriptionRequest; +import com.dedicatedcode.reitti.dto.SubscriptionResponse; import com.dedicatedcode.reitti.dto.TimelineEntry; +import com.dedicatedcode.reitti.model.NotificationData; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.repository.UserJdbcService; import com.dedicatedcode.reitti.service.TimelineService; +import com.dedicatedcode.reitti.service.UserNotificationService; import com.dedicatedcode.reitti.service.VersionService; +import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService; +import com.dedicatedcode.reitti.service.integration.ReittiSubscription; +import com.dedicatedcode.reitti.service.integration.ReittiSubscriptionService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.List; +import java.util.Optional; @RestController @RequestMapping("/api/v1/reitti-integration") public class ReittiIntegrationApiController { + private static final Logger log = LoggerFactory.getLogger(ReittiIntegrationApiController.class); private final VersionService versionService; private final TimelineService timelineService; + private final ReittiSubscriptionService subscriptionService; + private final ReittiIntegrationService integrationService; + private final UserNotificationService userNotificationService; + private final UserJdbcService userJdbcService; public ReittiIntegrationApiController(VersionService versionService, - TimelineService timelineService) { + TimelineService timelineService, + ReittiSubscriptionService subscriptionService, + ReittiIntegrationService integrationService, + UserNotificationService userNotificationService, + UserJdbcService userJdbcService) { this.versionService = versionService; this.timelineService = timelineService; + this.subscriptionService = subscriptionService; + this.integrationService = integrationService; + this.userNotificationService = userNotificationService; + this.userJdbcService = userJdbcService; } @GetMapping("/info") @@ -45,10 +68,37 @@ public class ReittiIntegrationApiController { LocalDate selectedDate = LocalDate.parse(date); ZoneId userTimezone = ZoneId.of(timezone); - // Convert LocalDate to start and end Instant for the selected date in user's timezone Instant startOfDay = selectedDate.atStartOfDay(userTimezone).toInstant(); Instant endOfDay = selectedDate.plusDays(1).atStartOfDay(userTimezone).toInstant().minusMillis(1); return this.timelineService.buildTimelineEntries(user, userTimezone, selectedDate, startOfDay, endOfDay); } + + @PostMapping("/subscribe") + public ResponseEntity subscribe(@AuthenticationPrincipal User user, + @Valid @RequestBody SubscriptionRequest request) { + SubscriptionResponse response = subscriptionService.createSubscription(user, request.getCallbackUrl()); + return ResponseEntity.ok(response); + } + + @PostMapping("/notify/{subscriptionId}") + public ResponseEntity notify(@PathVariable String subscriptionId, + @RequestBody NotificationData notificationData) { + try { + Optional userId = this.integrationService.getUserIdForSubscription(subscriptionId); + + if (userId.isEmpty()) { + log.warn("Subscription with id {} not found", subscriptionId); + return ResponseEntity.notFound().build(); + } + + this.userJdbcService.findById(userId.get()).ifPresentOrElse(user -> { + this.userNotificationService.sendToQueue(user, notificationData.getAffectedDates(), notificationData.getEventType()); + }, () -> log.warn("Unable to find user for [{}]", subscriptionId)); + + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } } diff --git a/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionRequest.java b/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionRequest.java new file mode 100644 index 00000000..43a279e3 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionRequest.java @@ -0,0 +1,18 @@ +package com.dedicatedcode.reitti.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public class SubscriptionRequest { + @NotBlank + @Pattern(regexp = "^https?://.*", message = "Callback URL must be a valid HTTP/HTTPS URL") + private String callbackUrl; + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callbackUrl) { + this.callbackUrl = callbackUrl; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionResponse.java b/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionResponse.java new file mode 100644 index 00000000..7b29cc37 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/dto/SubscriptionResponse.java @@ -0,0 +1,27 @@ +package com.dedicatedcode.reitti.dto; + +import java.time.Instant; + +public class SubscriptionResponse { + private final String subscriptionId; + private final String status; + private final Instant createdAt; + + public SubscriptionResponse(String subscriptionId, String status, Instant createdAt) { + this.subscriptionId = subscriptionId; + this.status = status; + this.createdAt = createdAt; + } + + public String getSubscriptionId() { + return subscriptionId; + } + + public String getStatus() { + return status; + } + + public Instant getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/model/GeoPoint.java b/src/main/java/com/dedicatedcode/reitti/model/GeoPoint.java index ab8d8869..efe0a7e0 100644 --- a/src/main/java/com/dedicatedcode/reitti/model/GeoPoint.java +++ b/src/main/java/com/dedicatedcode/reitti/model/GeoPoint.java @@ -7,6 +7,10 @@ public record GeoPoint(double latitude, double longitude) { return new GeoPoint(point.getY(), point.getX()); } + public static GeoPoint from(double latitude, double longitude) { + return new GeoPoint(latitude, longitude); + } + public boolean near(GeoPoint point) { return GeoUtils.distanceInMeters(this, point) < 100; } diff --git a/src/main/java/com/dedicatedcode/reitti/model/NotificationData.java b/src/main/java/com/dedicatedcode/reitti/model/NotificationData.java new file mode 100644 index 00000000..8903cc28 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/model/NotificationData.java @@ -0,0 +1,30 @@ +package com.dedicatedcode.reitti.model; + +import com.dedicatedcode.reitti.event.SSEType; + +import java.time.LocalDate; +import java.util.Set; + +public class NotificationData { + private final SSEType eventType; + private final Long userId; + private final Set affectedDates; + + public NotificationData(SSEType eventType, Long userId, Set affectedDates) { + this.eventType = eventType; + this.userId = userId; + this.affectedDates = affectedDates; + } + + public SSEType getEventType() { + return eventType; + } + + public Long getUserId() { + return userId; + } + + public Set getAffectedDates() { + return affectedDates; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/model/SignificantPlace.java b/src/main/java/com/dedicatedcode/reitti/model/SignificantPlace.java index 905b9fe4..c6d65c7f 100644 --- a/src/main/java/com/dedicatedcode/reitti/model/SignificantPlace.java +++ b/src/main/java/com/dedicatedcode/reitti/model/SignificantPlace.java @@ -1,7 +1,5 @@ package com.dedicatedcode.reitti.model; -import org.locationtech.jts.geom.Point; - import java.io.Serializable; import java.util.Objects; @@ -13,24 +11,26 @@ public class SignificantPlace implements Serializable { private final String countryCode; private final Double latitudeCentroid; private final Double longitudeCentroid; - private final Point geom; private final PlaceType type; private final boolean geocoded; private final Long version; - public static SignificantPlace create(Double latitude, Double longitude, Point point) { - return new SignificantPlace(null, null, latitude, longitude, point, PlaceType.OTHER, null); + public static SignificantPlace create(Double latitude, Double longitude) { + return new SignificantPlace(null, null, latitude, longitude, PlaceType.OTHER, null); + } + + public SignificantPlace() { + this(null, null, null, null, null, null, null, false, null); } private SignificantPlace(String name, String address, Double latitudeCentroid, Double longitudeCentroid, - Point geom, PlaceType type, String countryCode) { - this(null, name, address, countryCode, latitudeCentroid, longitudeCentroid, geom, type, false, 1L); + this(null, name, address, countryCode, latitudeCentroid, longitudeCentroid, type, false, 1L); } public SignificantPlace(Long id, @@ -39,7 +39,6 @@ public class SignificantPlace implements Serializable { String countryCode, Double latitudeCentroid, Double longitudeCentroid, - Point geom, PlaceType type, boolean geocoded, Long version) { @@ -49,7 +48,6 @@ public class SignificantPlace implements Serializable { this.countryCode = countryCode; this.latitudeCentroid = latitudeCentroid; this.longitudeCentroid = longitudeCentroid; - this.geom = geom; this.type = type; this.geocoded = geocoded; this.version = version; @@ -84,11 +82,7 @@ public class SignificantPlace implements Serializable { return type; } - public Point getGeom() { - return geom; - } - - public boolean isGeocoded() { + public boolean isGeocoded() { return geocoded; } @@ -98,27 +92,27 @@ public class SignificantPlace implements Serializable { // Wither methods public SignificantPlace withGeocoded(boolean geocoded) { - return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, this.type, geocoded, this.version); + return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, geocoded, this.version); } public SignificantPlace withName(String name) { - return new SignificantPlace(this.id, name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, this.type, this.geocoded, this.version); + return new SignificantPlace(this.id, name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version); } public SignificantPlace withAddress(String address) { - return new SignificantPlace(this.id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, this.type, this.geocoded, this.version); + return new SignificantPlace(this.id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version); } public SignificantPlace withCountryCode(String countryCode) { - return new SignificantPlace(this.id, this.name, this.address, countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, this.type, this.geocoded, this.version); + return new SignificantPlace(this.id, this.name, this.address, countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version); } public SignificantPlace withType(PlaceType type) { - return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, type, this.geocoded, this.version); + return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, type, this.geocoded, this.version); } public SignificantPlace withId(Long id) { - return new SignificantPlace(id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.geom, this.type, this.geocoded, this.version); + return new SignificantPlace(id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version); } @Override @@ -138,7 +132,6 @@ public class SignificantPlace implements Serializable { return "SignificantPlace{" + "id=" + id + ", name='" + name + '\'' + - ", geom=" + geom + '}'; } diff --git a/src/main/java/com/dedicatedcode/reitti/model/User.java b/src/main/java/com/dedicatedcode/reitti/model/User.java index 3d75d29d..59d3b5eb 100644 --- a/src/main/java/com/dedicatedcode/reitti/model/User.java +++ b/src/main/java/com/dedicatedcode/reitti/model/User.java @@ -6,6 +6,7 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; +import java.util.Objects; public class User implements UserDetails { @@ -101,4 +102,16 @@ public class User implements UserDetails { public User withRole(Role role) { return new User(this.id, this.username, this.password, this.displayName, role, this.version); } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } } diff --git a/src/main/java/com/dedicatedcode/reitti/repository/PointReaderWriter.java b/src/main/java/com/dedicatedcode/reitti/repository/PointReaderWriter.java index 85a4a266..8b7334f5 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/PointReaderWriter.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/PointReaderWriter.java @@ -1,5 +1,6 @@ package com.dedicatedcode.reitti.repository; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.locationtech.jts.io.ParseException; @@ -10,8 +11,11 @@ import org.springframework.stereotype.Component; public class PointReaderWriter { private final WKTReader wktReader; + private final GeometryFactory geometryFactory; + public PointReaderWriter(GeometryFactory geometryFactory) { this.wktReader = new WKTReader(geometryFactory); + this.geometryFactory = geometryFactory; } public Point read(String wkt) { @@ -20,6 +24,9 @@ public class PointReaderWriter { } catch (ParseException e) { throw new RuntimeException(e); } + } + public String write(double x, double y) { + return geometryFactory.createPoint(new Coordinate(x, y)).toString(); } } diff --git a/src/main/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcService.java b/src/main/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcService.java index 2fc98bd4..7ca6617b 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcService.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcService.java @@ -20,9 +20,11 @@ import java.util.Optional; public class SignificantPlaceJdbcService { private final JdbcTemplate jdbcTemplate; + private final PointReaderWriter pointReaderWriter; public SignificantPlaceJdbcService(JdbcTemplate jdbcTemplate, PointReaderWriter pointReaderWriter) { this.jdbcTemplate = jdbcTemplate; + this.pointReaderWriter = pointReaderWriter; this.significantPlaceRowMapper = (rs, _) -> new SignificantPlace( rs.getLong("id"), rs.getString("name"), @@ -30,7 +32,6 @@ public class SignificantPlaceJdbcService { rs.getString("country_code"), rs.getDouble("latitude_centroid"), rs.getDouble("longitude_centroid"), - pointReaderWriter.read(rs.getString("geom")), SignificantPlace.PlaceType.valueOf(rs.getString("type")), rs.getBoolean("geocoded"), rs.getLong("version")); @@ -69,7 +70,7 @@ public class SignificantPlaceJdbcService { place.getName(), place.getLatitudeCentroid(), place.getLongitudeCentroid(), - place.getGeom().toString() + this.pointReaderWriter.write(place.getLongitudeCentroid(), place.getLatitudeCentroid()) ); return place.withId(id); } @@ -84,7 +85,7 @@ public class SignificantPlaceJdbcService { place.getType().name(), place.getLatitudeCentroid(), place.getLongitudeCentroid(), - place.getGeom().toString(), + this.pointReaderWriter.write(place.getLongitudeCentroid(), place.getLatitudeCentroid()), place.isGeocoded(), place.getId() ); diff --git a/src/main/java/com/dedicatedcode/reitti/service/MessageDispatcherService.java b/src/main/java/com/dedicatedcode/reitti/service/MessageDispatcherService.java index 1ba0dcc7..31bf97b0 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/MessageDispatcherService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/MessageDispatcherService.java @@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service; import com.dedicatedcode.reitti.config.RabbitMQConfig; import com.dedicatedcode.reitti.event.*; +import com.dedicatedcode.reitti.repository.UserJdbcService; import com.dedicatedcode.reitti.service.geocoding.ReverseGeocodingListener; import com.dedicatedcode.reitti.service.processing.*; import org.slf4j.Logger; @@ -22,6 +23,7 @@ public class MessageDispatcherService { private final ReverseGeocodingListener reverseGeocodingListener; private final ProcessingPipelineTrigger processingPipelineTrigger; private final UserSseEmitterService userSseEmitterService; + private final UserJdbcService userJdbcService; @Autowired public MessageDispatcherService(LocationDataIngestPipeline locationDataIngestPipeline, @@ -30,7 +32,8 @@ public class MessageDispatcherService { TripDetectionService tripDetectionService, ReverseGeocodingListener reverseGeocodingListener, ProcessingPipelineTrigger processingPipelineTrigger, - UserSseEmitterService userSseEmitterService) { + UserSseEmitterService userSseEmitterService, + UserJdbcService userJdbcService) { this.locationDataIngestPipeline = locationDataIngestPipeline; this.visitDetectionService = visitDetectionService; this.visitMergingService = visitMergingService; @@ -38,6 +41,7 @@ public class MessageDispatcherService { this.reverseGeocodingListener = reverseGeocodingListener; this.processingPipelineTrigger = processingPipelineTrigger; this.userSseEmitterService = userSseEmitterService; + this.userJdbcService = userJdbcService; } @RabbitListener(queues = RabbitMQConfig.LOCATION_DATA_QUEUE, concurrency = "${reitti.events.concurrency}") @@ -73,7 +77,7 @@ public class MessageDispatcherService { @RabbitListener(queues = RabbitMQConfig.USER_EVENT_QUEUE) public void handleUserNotificationEvent(SSEEvent event) { logger.debug("Dispatching SSEEvent for user: {}", event.getUserId()); - this.userSseEmitterService.sendEventToUser(event.getUserId(), event); + this.userJdbcService.findById(event.getUserId()).ifPresentOrElse(user -> this.userSseEmitterService.sendEventToUser(user, event), () -> logger.warn("User not found for user: {}", event.getUserId())); } @RabbitListener(queues = RabbitMQConfig.TRIGGER_PROCESSING_PIPELINE_QUEUE, concurrency = "${reitti.events.concurrency}") diff --git a/src/main/java/com/dedicatedcode/reitti/service/UserNotificationQueueService.java b/src/main/java/com/dedicatedcode/reitti/service/UserNotificationService.java similarity index 70% rename from src/main/java/com/dedicatedcode/reitti/service/UserNotificationQueueService.java rename to src/main/java/com/dedicatedcode/reitti/service/UserNotificationService.java index d1ed872c..d220693e 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/UserNotificationQueueService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/UserNotificationService.java @@ -4,10 +4,11 @@ import com.dedicatedcode.reitti.config.RabbitMQConfig; import com.dedicatedcode.reitti.dto.LocationDataRequest; import com.dedicatedcode.reitti.event.SSEEvent; import com.dedicatedcode.reitti.event.SSEType; +import com.dedicatedcode.reitti.model.NotificationData; import com.dedicatedcode.reitti.model.ProcessedVisit; import com.dedicatedcode.reitti.model.Trip; import com.dedicatedcode.reitti.model.User; -import com.dedicatedcode.reitti.repository.UserSettingsJdbcService; +import com.dedicatedcode.reitti.service.integration.ReittiSubscriptionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -23,15 +24,15 @@ import java.util.Set; import java.util.stream.Collectors; @Service -public class UserNotificationQueueService { - private static final Logger log = LoggerFactory.getLogger(UserNotificationQueueService.class); - private final UserSettingsJdbcService userSettingsJdbcService; +public class UserNotificationService { + private static final Logger log = LoggerFactory.getLogger(UserNotificationService.class); private final RabbitTemplate rabbitTemplate; + private final ReittiSubscriptionService reittiSubscriptionService; - public UserNotificationQueueService(UserSettingsJdbcService userSettingsJdbcService, - RabbitTemplate rabbitTemplate) { - this.userSettingsJdbcService = userSettingsJdbcService; + public UserNotificationService(RabbitTemplate rabbitTemplate, + ReittiSubscriptionService reittiSubscriptionService) { this.rabbitTemplate = rabbitTemplate; + this.reittiSubscriptionService = reittiSubscriptionService; } public void newTrips(User user, List trips) { @@ -39,6 +40,7 @@ public class UserNotificationQueueService { log.debug("New trips for user [{}]", user.getId()); Set dates = calculateAffectedDates(trips.stream().map(Trip::getStartTime).toList(), trips.stream().map(Trip::getEndTime).toList()); sendToQueue(user, dates, eventType); + notifyReittiSubscriptions(user, eventType, dates); } public void newVisits(User user, List processedVisits) { @@ -46,6 +48,7 @@ public class UserNotificationQueueService { log.debug("New Visits for user [{}]", user.getId()); Set dates = calculateAffectedDates(processedVisits.stream().map(ProcessedVisit::getStartTime).toList(), processedVisits.stream().map(ProcessedVisit::getEndTime).toList()); sendToQueue(user, dates, eventType); + notifyReittiSubscriptions(user, eventType, dates); } public void newRawLocationData(User user, List filtered) { @@ -53,14 +56,24 @@ public class UserNotificationQueueService { log.debug("New RawLocationPoints for user [{}]", user.getId()); Set dates = calculateAffectedDates(filtered.stream().map(LocationDataRequest.LocationPoint::getTimestamp).map(s -> ZonedDateTime.parse(s).toInstant()).toList()); sendToQueue(user, dates, eventType); + notifyReittiSubscriptions(user, eventType, dates); } - private void sendToQueue(User user, Set dates, SSEType eventType) { + public void sendToQueue(User user, Set dates, SSEType eventType) { for (LocalDate date : dates) { this.rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.USER_EVENT_ROUTING_KEY, new SSEEvent(eventType, user.getId(), user.getId(), date)); } } + private void notifyReittiSubscriptions(User user, SSEType eventType, Set dates) { + try { + NotificationData notificationData = new NotificationData(eventType, user.getId(), dates); + reittiSubscriptionService.notifyAllSubscriptions(user, notificationData); + } catch (Exception e) { + log.error("Failed to notify Reitti subscriptions for user: {}", user.getId(), e); + } + } + @SafeVarargs private Set calculateAffectedDates(List... list) { if (list == null) { diff --git a/src/main/java/com/dedicatedcode/reitti/service/UserSseEmitterService.java b/src/main/java/com/dedicatedcode/reitti/service/UserSseEmitterService.java index f26c5365..bee5444a 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/UserSseEmitterService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/UserSseEmitterService.java @@ -1,6 +1,8 @@ package com.dedicatedcode.reitti.service; import com.dedicatedcode.reitti.event.SSEEvent; +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.SmartLifecycle; @@ -16,68 +18,63 @@ import java.util.concurrent.CopyOnWriteArraySet; @Service public class UserSseEmitterService implements SmartLifecycle { private static final Logger log = LoggerFactory.getLogger(UserSseEmitterService.class); - private final Map> userEmitters = new ConcurrentHashMap<>(); + private final ReittiIntegrationService reittiIntegrationService; + private final Map> userEmitters = new ConcurrentHashMap<>(); - public SseEmitter addEmitter(Long userId) { + public UserSseEmitterService(ReittiIntegrationService reittiIntegrationService) { + this.reittiIntegrationService = reittiIntegrationService; + } + + public SseEmitter addEmitter(User user) { SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); - userEmitters.computeIfAbsent(userId, k -> new CopyOnWriteArraySet<>()).add(emitter); + userEmitters.computeIfAbsent(user, _ -> new CopyOnWriteArraySet<>()).add(emitter); emitter.onCompletion(() -> { - log.info("SSE connection completed for user: [{}]", userId); - removeEmitter(userId, emitter); + log.info("SSE connection completed for user: [{}]", user); + removeEmitter(user, emitter); }); emitter.onTimeout(() -> { - log.info("SSE connection timed out for user: [{}]", userId); - emitter.complete(); // Complete the emitter on timeout - removeEmitter(userId, emitter); + log.info("SSE connection timed out for user: [{}]", user); + emitter.complete(); + removeEmitter(user, emitter); }); emitter.onError(throwable -> { - log.error("SSE connection error for user [{}]: {}", userId, throwable.getMessage()); - removeEmitter(userId, emitter); + log.error("SSE connection error for user [{}]: {}", user, throwable.getMessage()); + removeEmitter(user, emitter); }); - log.info("Emitter added for user: {}. Total emitters for user: {}", userId, userEmitters.get(userId).size()); + log.info("Emitter added for user: {}. Total emitters for user: {}", user, userEmitters.get(user).size()); return emitter; } - public void sendEventToUser(Long userId, SSEEvent eventData) { - Set emitters = userEmitters.get(userId); + public void sendEventToUser(User user, SSEEvent eventData) { + Set emitters = userEmitters.get(user); if (emitters != null) { for (SseEmitter emitter : new CopyOnWriteArraySet<>(emitters)) { try { emitter.send(SseEmitter.event().data(eventData)); - log.debug("Sent event to user: {}", userId); + log.debug("Sent event to user: {}", user); } catch (IOException e) { - log.error("Error sending event to user {}: {}", userId, e.getMessage()); + log.error("Error sending event to user {}: {}", user, e.getMessage()); emitter.completeWithError(e); - removeEmitter(userId, emitter); + removeEmitter(user, emitter); } } } else { - log.debug("No active SSE emitters for user: {}", userId); + log.debug("No active SSE emitters for user: {}", user); } } - private void removeEmitter(Long userId, SseEmitter emitter) { - Set emitters = userEmitters.get(userId); + private void removeEmitter(User user, SseEmitter emitter) { + Set emitters = userEmitters.get(user); if (emitters != null) { emitters.remove(emitter); if (emitters.isEmpty()) { - userEmitters.remove(userId); - } - log.info("Emitter removed for user: {}. Remaining emitters for user: {}", userId, userEmitters.containsKey(userId) ? userEmitters.get(userId).size() : 0); - } - } - - public void removeEmitter(Long userId) { - Set emitters = userEmitters.get(userId); - if (emitters != null) { - for (SseEmitter emitter : emitters) { - removeEmitter(userId, emitter); - } - userEmitters.remove(userId); - log.info("Removed all emitters for user: {}. Remaining emitters for user: {}", userId, userEmitters.containsKey(userId) ? userEmitters.get(userId).size() : 0); + userEmitters.remove(user); + reittiIntegrationService.unsubscribeFromIntegrations(user); + } + log.info("Emitter removed for user: {}. Remaining emitters for user: {}", user, userEmitters.containsKey(user) ? userEmitters.get(user).size() : 0); } } diff --git a/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java index 5ff433be..d35d0840 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java @@ -1,9 +1,6 @@ package com.dedicatedcode.reitti.service.integration; -import com.dedicatedcode.reitti.dto.LocationDataRequest; -import com.dedicatedcode.reitti.dto.ReittiRemoteInfo; -import com.dedicatedcode.reitti.dto.TimelineEntry; -import com.dedicatedcode.reitti.dto.UserTimelineData; +import com.dedicatedcode.reitti.dto.*; import com.dedicatedcode.reitti.model.ReittiIntegration; import com.dedicatedcode.reitti.model.RemoteUser; import com.dedicatedcode.reitti.model.User; @@ -14,6 +11,7 @@ import com.dedicatedcode.reitti.service.RequestFailedException; import com.dedicatedcode.reitti.service.RequestTemporaryFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -28,19 +26,24 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; @Service public class ReittiIntegrationService { private static final Logger log = LoggerFactory.getLogger(ReittiIntegrationService.class); private static final List VALID_INTEGRATION_STATUS = List.of(ReittiIntegration.Status.ACTIVE, ReittiIntegration.Status.RECOVERABLE); + private final String advertiseUri; private final ReittiIntegrationJdbcService jdbcService; - private final RestTemplate restTemplate; private final AvatarService avatarService; + private final Map integrationSubscriptions = new ConcurrentHashMap<>(); + private final Map userForSubscriptions = new ConcurrentHashMap<>(); - public ReittiIntegrationService(ReittiIntegrationJdbcService jdbcService, - RestTemplate restTemplate, AvatarService avatarService) { + public ReittiIntegrationService(@Value("${reitti.server.advertise-uri}") String advertiseUri, ReittiIntegrationJdbcService jdbcService, + RestTemplate restTemplate, + AvatarService avatarService) { + this.advertiseUri = advertiseUri; this.jdbcService = jdbcService; this.restTemplate = restTemplate; this.avatarService = avatarService; @@ -56,7 +59,7 @@ public class ReittiIntegrationService { try { RemoteUser remoteUser = handleRemoteUser(integration); List timelineEntries = loadTimeLineEntries(integration, selectedDate, userTimezone); - update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now())); + integration = update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now())); return new UserTimelineData("remote:" + integration.getId(), remoteUser.getDisplayName(), this.avatarService.generateInitials(remoteUser.getDisplayName()), @@ -155,6 +158,7 @@ public class ReittiIntegrationService { .findFirst().orElse(Collections.emptyList()); } + private ReittiIntegration update(ReittiIntegration integration) { try { return this.jdbcService.update(integration).orElseThrow(); @@ -232,4 +236,133 @@ public class ReittiIntegrationService { return persisted.get(); } -} + public void registerSubscriptionsForUser(User user) { + log.info("Registering subscriptions for user: [{}]", user.getId()); + + if (advertiseUri == null || advertiseUri.isEmpty()) { + log.warn("Advertise URI is null or empty, remote updates are disabled. Consider setting 'reitti.server.advertise-uri'"); + return; + } + + List activeIntegrations = getActiveIntegrationsForUser(user); + + for (ReittiIntegration integration : activeIntegrations) { + try { + registerSubscriptionOnIntegration(integration, user); + log.debug("Successfully registered subscription for integration: [{}]", integration.getId()); + } catch (Exception | RequestFailedException e) { + log.error("couldn't fetch user info for [{}]", integration, e); + update(integration.withStatus(ReittiIntegration.Status.FAILED).withLastUsed(LocalDateTime.now()).withEnabled(false)); + } catch (RequestTemporaryFailedException e) { + log.warn("couldn't temporarily fetch user info for [{}]", integration, e); + update(integration.withStatus(ReittiIntegration.Status.RECOVERABLE).withLastUsed(LocalDateTime.now())); + } + } + } + + public List getActiveIntegrationsForUser(User user) { + return this.jdbcService + .findAllByUser(user) + .stream() + .filter(integration -> integration.isEnabled() && VALID_INTEGRATION_STATUS.contains(integration.getStatus())) + .toList(); + } + + private void registerSubscriptionOnIntegration(ReittiIntegration integration, User user) throws RequestFailedException, RequestTemporaryFailedException { + if (advertiseUri == null || advertiseUri.isEmpty()) { + log.warn("No advertise URI configured, skipping subscription registration for integration: [{}]", integration.getId()); + return; + } + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-API-TOKEN", integration.getToken()); + headers.setContentType(MediaType.APPLICATION_JSON); + + SubscriptionRequest subscriptionRequest = new SubscriptionRequest(); + subscriptionRequest.setCallbackUrl(advertiseUri); + HttpEntity entity = new HttpEntity<>(subscriptionRequest, headers); + + String subscribeUrl = integration.getUrl().endsWith("/") ? + integration.getUrl() + "api/v1/reitti-integration/subscribe" : + integration.getUrl() + "/api/v1/reitti-integration/subscribe"; + + try { + ResponseEntity response = restTemplate.exchange( + subscribeUrl, + HttpMethod.POST, + entity, + SubscriptionResponse.class + ); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + log.debug("Successfully subscribed to integration: [{}]", integration.getId()); + synchronized (integrationSubscriptions) { + this.integrationSubscriptions.put(integration.getId(), response.getBody().getSubscriptionId()); + this.userForSubscriptions.put(response.getBody().getSubscriptionId(), user.getId()); + } + } else if (response.getStatusCode().is4xxClientError()) { + throw new RequestFailedException(subscribeUrl, response.getStatusCode(), response.getBody()); + } else { + throw new RequestTemporaryFailedException(subscribeUrl, response.getStatusCode(), response.getBody()); + } + } catch (RestClientException ex) { + throw new RequestFailedException(subscribeUrl, HttpStatusCode.valueOf(500), "Connection refused"); + } + } + + public void unsubscribeFromIntegrations(User user) { + log.info("Unsubscribing from integrations for user: [{}]", user.getId()); + + List activeIntegrations = getActiveIntegrationsForUser(user); + + for (ReittiIntegration integration : activeIntegrations) { + String subscriptionId = integrationSubscriptions.get(integration.getId()); + if (subscriptionId != null) { + try { + unsubscribeFromIntegration(integration, subscriptionId); + integrationSubscriptions.remove(integration.getId()); + userForSubscriptions.remove(subscriptionId); + log.debug("Successfully unsubscribed from integration: [{}]", integration.getId()); + } catch (Exception | RequestFailedException e) { + log.warn("Failed to unsubscribe from integration: [{}]", integration.getId(), e); + update(integration.withStatus(ReittiIntegration.Status.FAILED).withLastUsed(LocalDateTime.now()).withEnabled(false)); + } catch (RequestTemporaryFailedException e) { + update(integration.withStatus(ReittiIntegration.Status.RECOVERABLE).withLastUsed(LocalDateTime.now())); + } + } + } + } + + private void unsubscribeFromIntegration(ReittiIntegration integration, String subscriptionId) throws RequestFailedException, RequestTemporaryFailedException { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-API-TOKEN", integration.getToken()); + HttpEntity entity = new HttpEntity<>(headers); + + String unsubscribeUrl = integration.getUrl().endsWith("/") ? + integration.getUrl() + "api/v1/reitti-integration/subscribe/" + subscriptionId : + integration.getUrl() + "/api/v1/reitti-integration/subscribe/" + subscriptionId; + + try { + ResponseEntity response = restTemplate.exchange( + unsubscribeUrl, + HttpMethod.DELETE, + entity, + Void.class + ); + + if (!response.getStatusCode().is2xxSuccessful()) { + if (response.getStatusCode().is4xxClientError()) { + throw new RequestFailedException(unsubscribeUrl, response.getStatusCode(), null); + } else { + throw new RequestTemporaryFailedException(unsubscribeUrl, response.getStatusCode(), null); + } + } + } catch (RestClientException ex) { + throw new RequestFailedException(unsubscribeUrl, HttpStatusCode.valueOf(500), "Connection refused"); + } + } + + public Optional getUserIdForSubscription(String subscriptionId) { + return Optional.ofNullable(this.userForSubscriptions.get(subscriptionId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscription.java b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscription.java new file mode 100644 index 00000000..6eb02e4f --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscription.java @@ -0,0 +1,25 @@ +package com.dedicatedcode.reitti.service.integration; + +public class ReittiSubscription { + private final String subscriptionId; + private final Long userId; + private final String callbackUrl; + + public ReittiSubscription(String subscriptionId, Long userId, String callbackUrl) { + this.subscriptionId = subscriptionId; + this.userId = userId; + this.callbackUrl = callbackUrl; + } + + public String getSubscriptionId() { + return subscriptionId; + } + + public Long getUserId() { + return userId; + } + + public String getCallbackUrl() { + return callbackUrl; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscriptionService.java b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscriptionService.java new file mode 100644 index 00000000..2e3b0b92 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiSubscriptionService.java @@ -0,0 +1,66 @@ +package com.dedicatedcode.reitti.service.integration; + +import com.dedicatedcode.reitti.dto.SubscriptionResponse; +import com.dedicatedcode.reitti.model.NotificationData; +import com.dedicatedcode.reitti.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class ReittiSubscriptionService { + private static final Logger log = LoggerFactory.getLogger(ReittiSubscriptionService.class); + private final Map subscriptions = new ConcurrentHashMap<>(); + private final RestTemplate restTemplate; + + public ReittiSubscriptionService() { + this.restTemplate = new RestTemplate(); + } + + public SubscriptionResponse createSubscription(User user, String callbackUrl) { + String subscriptionId = "sub_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + Instant now = Instant.now(); + + ReittiSubscription subscription = new ReittiSubscription(subscriptionId, user.getId(), callbackUrl); + subscriptions.put(subscriptionId, subscription); + + return new SubscriptionResponse(subscriptionId, "active", now); + } + + public ReittiSubscription getSubscription(String subscriptionId) { + return subscriptions.get(subscriptionId); + } + + public void notifyAllSubscriptions(User user, NotificationData notificationData) { + subscriptions.values().stream() + .filter(subscription -> subscription.getUserId().equals(user.getId())) + .forEach(subscription -> sendNotificationToCallback(subscription, notificationData)); + } + + private void sendNotificationToCallback(ReittiSubscription subscription, NotificationData notificationData) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity request = new HttpEntity<>(notificationData, headers); + + String notifyUrl = subscription.getCallbackUrl().endsWith("/") ? + subscription.getCallbackUrl() + "api/v1/reitti-integration/notify/" + subscription.getSubscriptionId() : + subscription.getCallbackUrl() + "/api/v1/reitti-integration/notify/" + subscription.getSubscriptionId(); + restTemplate.postForEntity(notifyUrl, request, String.class); + log.debug("Notification sent successfully to subscription: {}", subscription.getSubscriptionId()); + } catch (Exception e) { + log.error("Failed to send notification to subscription: {}, callback URL: {}", + subscription.getSubscriptionId(), subscription.getCallbackUrl(), e); + this.subscriptions.remove(subscription.getSubscriptionId()); + } + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/processing/LocationDataIngestPipeline.java b/src/main/java/com/dedicatedcode/reitti/service/processing/LocationDataIngestPipeline.java index 5ca0b9be..96cddcd4 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/LocationDataIngestPipeline.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/LocationDataIngestPipeline.java @@ -6,7 +6,7 @@ import com.dedicatedcode.reitti.model.User; import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService; import com.dedicatedcode.reitti.repository.UserJdbcService; import com.dedicatedcode.reitti.repository.UserSettingsJdbcService; -import com.dedicatedcode.reitti.service.UserNotificationQueueService; +import com.dedicatedcode.reitti.service.UserNotificationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,19 +23,19 @@ public class LocationDataIngestPipeline { private final UserJdbcService userJdbcService; private final RawLocationPointJdbcService rawLocationPointJdbcService; private final UserSettingsJdbcService userSettingsJdbcService; - private final UserNotificationQueueService userNotificationQueueService; + private final UserNotificationService userNotificationService; @Autowired public LocationDataIngestPipeline(GeoPointAnomalyFilter geoPointAnomalyFilter, UserJdbcService userJdbcService, RawLocationPointJdbcService rawLocationPointJdbcService, UserSettingsJdbcService userSettingsJdbcService, - UserNotificationQueueService userNotificationQueueService) { + UserNotificationService userNotificationService) { this.geoPointAnomalyFilter = geoPointAnomalyFilter; this.userJdbcService = userJdbcService; this.rawLocationPointJdbcService = rawLocationPointJdbcService; this.userSettingsJdbcService = userSettingsJdbcService; - this.userNotificationQueueService = userNotificationQueueService; + this.userNotificationService = userNotificationService; } public void processLocationData(LocationDataEvent event) { @@ -53,7 +53,7 @@ public class LocationDataIngestPipeline { List filtered = this.geoPointAnomalyFilter.filterAnomalies(points); rawLocationPointJdbcService.bulkInsert(user, filtered); userSettingsJdbcService.updateNewestData(user, filtered); - userNotificationQueueService.newRawLocationData(user, filtered); + userNotificationService.newRawLocationData(user, filtered); logger.info("Finished storing points [{}] for user [{}] in [{}]ms. Filtered out [{}] points.", filtered.size(), event.getUsername(), System.currentTimeMillis() - start, points.size() - filtered.size()); } } diff --git a/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java b/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java index c967add7..9c2cc1b2 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java @@ -6,7 +6,7 @@ import com.dedicatedcode.reitti.repository.ProcessedVisitJdbcService; import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService; import com.dedicatedcode.reitti.repository.TripJdbcService; import com.dedicatedcode.reitti.repository.UserJdbcService; -import com.dedicatedcode.reitti.service.UserNotificationQueueService; +import com.dedicatedcode.reitti.service.UserNotificationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -28,19 +28,19 @@ public class TripDetectionService { private final RawLocationPointJdbcService rawLocationPointJdbcService; private final TripJdbcService tripJdbcService; private final UserJdbcService userJdbcService; - private final UserNotificationQueueService userNotificationQueueService; + private final UserNotificationService userNotificationService; private final ConcurrentHashMap userLocks = new ConcurrentHashMap<>(); public TripDetectionService(ProcessedVisitJdbcService processedVisitJdbcService, RawLocationPointJdbcService rawLocationPointJdbcService, TripJdbcService tripJdbcService, UserJdbcService userJdbcService, - UserNotificationQueueService userNotificationQueueService) { + UserNotificationService userNotificationService) { this.processedVisitJdbcService = processedVisitJdbcService; this.rawLocationPointJdbcService = rawLocationPointJdbcService; this.tripJdbcService = tripJdbcService; this.userJdbcService = userJdbcService; - this.userNotificationQueueService = userNotificationQueueService; + this.userNotificationService = userNotificationService; } public void visitCreated(ProcessedVisitCreatedEvent event) { @@ -78,7 +78,7 @@ public class TripDetectionService { } tripJdbcService.bulkInsert(user, trips); - userNotificationQueueService.newTrips(user, trips); + userNotificationService.newTrips(user, trips); }); } finally { userLock.unlock(); diff --git a/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java b/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java index 7208de28..fbb2b0a3 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java @@ -6,7 +6,7 @@ import com.dedicatedcode.reitti.event.SignificantPlaceCreatedEvent; import com.dedicatedcode.reitti.event.VisitUpdatedEvent; import com.dedicatedcode.reitti.model.*; import com.dedicatedcode.reitti.repository.*; -import com.dedicatedcode.reitti.service.UserNotificationQueueService; +import com.dedicatedcode.reitti.service.UserNotificationService; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; @@ -37,7 +37,7 @@ public class VisitMergingService { private final RawLocationPointJdbcService rawLocationPointJdbcService; private final GeometryFactory geometryFactory; private final RabbitTemplate rabbitTemplate; - private final UserNotificationQueueService userNotificationQueueService; + private final UserNotificationService userNotificationService; private final long mergeThresholdSeconds; private final long mergeThresholdMeters; private final int searchRangeExtensionInHours; @@ -50,7 +50,7 @@ public class VisitMergingService { SignificantPlaceJdbcService significantPlaceJdbcService, RawLocationPointJdbcService rawLocationPointJdbcService, GeometryFactory geometryFactory, - UserNotificationQueueService userNotificationQueueService, + UserNotificationService userNotificationService, @Value("${reitti.visit.merge-max-stay-search-extension-days:2}") int maxStaySearchExtensionInDays, @Value("${reitti.visit.merge-threshold-seconds:300}") long mergeThresholdSeconds, @Value("${reitti.visit.merge-threshold-meters:100}") long mergeThresholdMeters) { @@ -61,7 +61,7 @@ public class VisitMergingService { this.significantPlaceJdbcService = significantPlaceJdbcService; this.rawLocationPointJdbcService = rawLocationPointJdbcService; this.geometryFactory = geometryFactory; - this.userNotificationQueueService = userNotificationQueueService; + this.userNotificationService = userNotificationService; this.mergeThresholdSeconds = mergeThresholdSeconds; this.mergeThresholdMeters = mergeThresholdMeters; this.searchRangeExtensionInHours = maxStaySearchExtensionInDays * 24; @@ -123,7 +123,7 @@ public class VisitMergingService { logger.debug("Processed [{}] visits into [{}] merged visits for user: [{}]", allVisits.size(), processedVisits.size(), user.getUsername()); - this.userNotificationQueueService.newVisits(user, processedVisits); + this.userNotificationService.newVisits(user, processedVisits); } @@ -219,7 +219,7 @@ public class VisitMergingService { private SignificantPlace createSignificantPlace(User user, Visit visit) { Point point = geometryFactory.createPoint(new Coordinate(visit.getLongitude(), visit.getLatitude())); - SignificantPlace significantPlace = SignificantPlace.create(visit.getLatitude(), visit.getLongitude(), point); + SignificantPlace significantPlace = SignificantPlace.create(visit.getLatitude(), visit.getLongitude()); significantPlace = this.significantPlaceJdbcService.create(user, significantPlace); publishSignificantPlaceCreatedEvent(significantPlace); return significantPlace; diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 742f72b5..425ecc33 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -10,3 +10,8 @@ reitti.events.concurrency=4-12 reitti.import.batch-size=1000 spring.thymeleaf.cache=false + +reitti.server.advertise-uri=http://localhost:8080 + + +reitti.import.processing-idle-start-time=5 diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 85b561cc..6704b8e0 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -22,6 +22,8 @@ spring.security.oauth2.client.registration.oauth.client-secret=${OIDC_CLIENT_SEC spring.security.oauth2.client.provider.oauth.issuer-uri=${OIDC_ISSUER_URI:} spring.security.oauth2.client.registration.oauth.scope=${OIDC_SCOPE:openid,profile} +reitti.server.advertise-uri=${ADVERTISE_URI:} + reitti.data-management.enabled=${DANGEROUS_LIFE:false} reitti.import.processing-idle-start-time=${PROCESSING_WAIT_TIME:15} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 428f2662..75ab4402 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -46,11 +46,14 @@ spring.servlet.multipart.max-file-size=5GB spring.servlet.multipart.max-request-size=5GB server.tomcat.max-part-count=100 + +# Application specific settings + +reitti.server.advertise-uri= # OAuth configuration # For now, we only support having one OIDC provider. If you need multiple, create a ticket in the reitti github. reitti.security.oidc.enabled=false -# Application specific settings reitti.import.batch-size=1000 # How many seconds should we wait after the last data input before starting to process all unprocessed data? reitti.import.processing-idle-start-time=15 diff --git a/src/main/resources/templates/fragments/export-data.html b/src/main/resources/templates/fragments/export-data.html index bd229c5e..19a08438 100644 --- a/src/main/resources/templates/fragments/export-data.html +++ b/src/main/resources/templates/fragments/export-data.html @@ -105,10 +105,5 @@ - - - diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 41a4b1ea..f4fa8f18 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -215,33 +215,57 @@ path.remove(); } - for (const element of timelineContainer) { + let bounds = L.latLngBounds(); + const fetchPromises = []; + + for (let i = 0; i < timelineContainer.length; i++) { + const element = timelineContainer[i]; const rawLocationPointsUrl = element?.dataset.rawLocationPointsUrl; const color = element?.dataset.baseColor; if (rawLocationPointsUrl) { - // Fetch raw location points for map display - fetch(rawLocationPointsUrl).then(response => { + // Create fetch promise for raw location points with index to maintain order + const fetchPromise = fetch(rawLocationPointsUrl).then(response => { if (!response.ok) { console.warn('Could not fetch raw location points'); - return { points: [] }; + return { points: [], index: i, color: color }; } return response.json(); }).then(rawPointsData => { - updateMapWithRawPoints(rawPointsData, color); - }).then(() => { - window.originalBounds = bounds; - map.fitBounds(bounds, fitToBoundsConfig) + return { ...rawPointsData, index: i, color: color }; }).catch(error => { console.warn('Error fetching raw location points:', error); + return { points: [], index: i, color: color }; // Return empty data with index on error }); + + fetchPromises.push(fetchPromise); } } - + // Wait for all fetch operations to complete, then update map in correct order + Promise.all(fetchPromises).then(results => { + // Sort results by original index to maintain order + results.sort((a, b) => a.index - b.index); + + // Process results in order + results.forEach(result => { + const fetchBounds = updateMapWithRawPoints(result, result.color); + if (fetchBounds.isValid()) { + bounds.extend(fetchBounds); + } + }); + + // Update map bounds after all fetch operations are complete + if (bounds.isValid()) { + window.originalBounds = bounds; + map.fitBounds(bounds, fitToBoundsConfig); + } + }); } // Function to update map with raw location points function updateMapWithRawPoints(rawPointsData, color) { + const bounds = L.latLngBounds(); + const rawPointsPath = L.geodesic([], { color: color == null ? '#f1ba63' : color, weight: 6, @@ -264,6 +288,8 @@ addPulsatingMarker(latestPoint.latitude, latestPoint.longitude, color); } } + + return bounds; } const selectedPath = L.geodesic([], { @@ -282,7 +308,6 @@ document.body.addEventListener('htmx:afterSwap', function(event) { if (event.detail.target.classList.contains('timeline-container')) { // Timeline content has been updated, update map markers - bounds = L.latLngBounds() loadTimelineData(getSelectedDate()) updateMapFromTimeline(); // Initialize scroll indicator after timeline is updated @@ -293,9 +318,10 @@ window.timelineScrollIndicator.init(); } }); - let bounds = L.latLngBounds(); // Function to update map markers from timeline entries function updateMapFromTimeline() { + const bounds = L.latLngBounds(); + // Clear existing markers and paths (except tile layer) map.eachLayer(layer => { if (!layer._url) { @@ -303,8 +329,6 @@ } }); - - window.originalBounds = L.latLngBounds(); let hasValidCoords = false; // Group places by coordinates to avoid duplicate markers @@ -404,6 +428,7 @@ }); } + return bounds; } // Helper function to parse duration text (simple implementation) @@ -688,7 +713,6 @@ // Schedule reload after 5 seconds of idle time reloadTimeoutId = setTimeout(() => { - debugger if (autoUpdateMode && pendingEvents.length > 0) { console.log(`Auto-update: Reloading timeline data after ${pendingEvents.length} accumulated events`); document.body.dispatchEvent(new CustomEvent('dateChanged')); diff --git a/src/test/java/com/dedicatedcode/reitti/repository/GeocodingResponseJdbcServiceTest.java b/src/test/java/com/dedicatedcode/reitti/repository/GeocodingResponseJdbcServiceTest.java index 1a99c3c2..c0e415a5 100644 --- a/src/test/java/com/dedicatedcode/reitti/repository/GeocodingResponseJdbcServiceTest.java +++ b/src/test/java/com/dedicatedcode/reitti/repository/GeocodingResponseJdbcServiceTest.java @@ -1,5 +1,6 @@ package com.dedicatedcode.reitti.repository; +import com.dedicatedcode.reitti.IntegrationTest; import com.dedicatedcode.reitti.TestingService; import com.dedicatedcode.reitti.model.GeocodingResponse; import com.dedicatedcode.reitti.model.SignificantPlace; @@ -17,7 +18,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest +@IntegrationTest @ActiveProfiles("test") @Transactional class GeocodingResponseJdbcServiceTest { @@ -26,7 +27,6 @@ class GeocodingResponseJdbcServiceTest { private GeocodingResponseJdbcService geocodingResponseJdbcService; @Autowired private SignificantPlaceJdbcService placeService; - @Autowired private GeometryFactory geometryFactory; @@ -40,7 +40,7 @@ class GeocodingResponseJdbcServiceTest { double latitudeCentroid = 53.863149; double longitudeCentroid = 10.700927; Point point = geometryFactory.createPoint(new Coordinate(longitudeCentroid, latitudeCentroid)); - SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid, point)); + SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid)); GeocodingResponse response = new GeocodingResponse( place.getId(), @@ -72,7 +72,7 @@ class GeocodingResponseJdbcServiceTest { double latitudeCentroid = 53.863149; double longitudeCentroid = 10.700927; Point point = geometryFactory.createPoint(new Coordinate(longitudeCentroid, latitudeCentroid)); - SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid, point)); + SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid)); // When List found = geocodingResponseJdbcService.findBySignificantPlace(place); @@ -88,7 +88,7 @@ class GeocodingResponseJdbcServiceTest { double latitudeCentroid = 53.863149; double longitudeCentroid = 10.700927; Point point = geometryFactory.createPoint(new Coordinate(longitudeCentroid, latitudeCentroid)); - SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid, point)); + SignificantPlace place = placeService.create(testingService.admin(), SignificantPlace.create(latitudeCentroid, longitudeCentroid)); GeocodingResponse response = new GeocodingResponse( place.getId(), diff --git a/src/test/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcServiceTest.java b/src/test/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcServiceTest.java index 52d4acfd..931a0764 100644 --- a/src/test/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcServiceTest.java +++ b/src/test/java/com/dedicatedcode/reitti/repository/SignificantPlaceJdbcServiceTest.java @@ -124,7 +124,6 @@ class SignificantPlaceJdbcServiceTest { "DE", 53.863149, 10.700927, - created.getGeom(), SignificantPlace.PlaceType.RESTAURANT, true, created.getVersion() @@ -207,7 +206,6 @@ class SignificantPlaceJdbcServiceTest { "DE", created1.getLatitudeCentroid(), created1.getLongitudeCentroid(), - created1.getGeom(), SignificantPlace.PlaceType.HOME, true, // geocoded = true created1.getVersion() @@ -254,7 +252,6 @@ class SignificantPlaceJdbcServiceTest { } private SignificantPlace createTestPlace(String name, double latitude, double longitude) { - Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude)); return new SignificantPlace( null, name, @@ -262,7 +259,6 @@ class SignificantPlaceJdbcServiceTest { null, latitude, longitude, - point, SignificantPlace.PlaceType.OTHER, false, 0L @@ -270,7 +266,6 @@ class SignificantPlaceJdbcServiceTest { } private SignificantPlace createTestPlaceForUser(User user, String name, double latitude, double longitude) { - Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude)); return new SignificantPlace( null, name, @@ -278,7 +273,6 @@ class SignificantPlaceJdbcServiceTest { null, latitude, longitude, - point, SignificantPlace.PlaceType.OTHER, false, 0L diff --git a/src/test/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManagerTest.java b/src/test/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManagerTest.java index 52fedb5d..3a116895 100644 --- a/src/test/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManagerTest.java +++ b/src/test/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManagerTest.java @@ -59,7 +59,7 @@ class DefaultGeocodeServiceManagerTest { .thenReturn(Collections.emptyList()); // When - Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(53.863149, 10.700927, null)); + Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(53.863149, 10.700927)); // Then assertThat(result).isEmpty(); @@ -100,7 +100,7 @@ class DefaultGeocodeServiceManagerTest { .thenReturn(mockResponse); // When - Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude, null)); + Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude)); // Then assertThat(result).isPresent(); @@ -135,7 +135,7 @@ class DefaultGeocodeServiceManagerTest { .thenReturn(mockResponse); // When - Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude, null)); + Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude)); // Then assertThat(result).isPresent(); @@ -217,7 +217,7 @@ class DefaultGeocodeServiceManagerTest { .thenReturn(mockResponse); // When - Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude, null)); + Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude)); // Then assertThat(result).isPresent(); @@ -269,7 +269,7 @@ class DefaultGeocodeServiceManagerTest { .thenReturn(photonResponse); // When - Optional result = managerWithFixedService.reverseGeocode(SignificantPlace.create(latitude, longitude, null)); + Optional result = managerWithFixedService.reverseGeocode(SignificantPlace.create(latitude, longitude)); // Then assertThat(result).isPresent(); @@ -300,7 +300,7 @@ class DefaultGeocodeServiceManagerTest { .thenThrow(new RuntimeException("Service unavailable")); // When - Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude, null)); + Optional result = geocodeServiceManager.reverseGeocode(SignificantPlace.create(latitude, longitude)); // Then assertThat(result).isEmpty(); diff --git a/src/test/java/com/dedicatedcode/reitti/service/processing/ProcessingPipelineTest.java b/src/test/java/com/dedicatedcode/reitti/service/processing/ProcessingPipelineTest.java index 90189680..fe3ff5ab 100644 --- a/src/test/java/com/dedicatedcode/reitti/service/processing/ProcessingPipelineTest.java +++ b/src/test/java/com/dedicatedcode/reitti/service/processing/ProcessingPipelineTest.java @@ -130,7 +130,7 @@ 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()); - GeoPoint currentLocation = GeoPoint.from(processedVisit.getPlace().getGeom()); + 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 + "]"); } @@ -146,11 +146,11 @@ public class ProcessingPipelineTest { assertEquals(Instant.parse(startTime), trip.getStartTime()); assertEquals(Instant.parse(endTime), trip.getEndTime()); - GeoPoint actualStartLocation = GeoPoint.from(trip.getStartVisit().getPlace().getGeom()); + GeoPoint actualStartLocation = GeoPoint.from(trip.getStartVisit().getPlace().getLatitudeCentroid(), trip.getStartVisit().getPlace().getLongitudeCentroid()); assertTrue(startLocation.near(actualStartLocation), "Start locations are not near to each other. \nExpected [" + actualStartLocation + "] to be in range \nto [" + startLocation + "]"); - GeoPoint actualEndLocation = GeoPoint.from(trip.getEndVisit().getPlace().getGeom()); + GeoPoint actualEndLocation = GeoPoint.from(trip.getEndVisit().getPlace().getLatitudeCentroid(), trip.getEndVisit().getPlace().getLongitudeCentroid()); assertTrue(endLocation.near(actualEndLocation), "End locations are not near to each other. \nExpected [" + actualEndLocation + "] to be in range \nto [" + endLocation + "]"); }