diff --git a/docs/tools/gpx-tools/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java b/docs/tools/gpx-tools/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java index fa63dcc4..63f5cab3 100644 --- a/docs/tools/gpx-tools/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java +++ b/docs/tools/gpx-tools/src/main/java/com/dedicatedcode/reitti/tools/GpxSender.java @@ -19,19 +19,19 @@ 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; @@ -48,7 +48,7 @@ public class GpxSender { public static void main(String[] args) { if (args.length < 1) { - System.err.println("Usage: java -jar gpx-sender.jar --url --token [--interval ] [--original-time]"); + System.err.println("Usage: java -jar gpx-sender.jar --url --token [--interval ] [--original-time] [--verbose]"); System.err.println("Example: java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --interval 15"); System.err.println(" java -jar gpx-sender.jar track.gpx --url http://localhost:8080 --token your-api-token --original-time"); System.exit(1); @@ -59,6 +59,7 @@ public class GpxSender { String apiToken = null; double intervalSeconds = 15.0; boolean useOriginalTime = false; + boolean verboseOutput = false; // Parse named parameters for (int i = 1; i < args.length; i++) { @@ -82,6 +83,10 @@ public class GpxSender { case "-ot": useOriginalTime = true; break; + case "--verbose": + case "-v": + verboseOutput = true; + break; default: System.err.println("Unknown parameter: " + args[i]); System.exit(1); @@ -107,8 +112,11 @@ public class GpxSender { } else { System.out.println("Interval: " + intervalSeconds + " seconds"); } + if (verboseOutput) { + System.out.println("Verbose output enabled"); + } - sendTrackPoints(trackPoints, reittiUrl, apiToken, intervalSeconds, useOriginalTime); + sendTrackPoints(trackPoints, reittiUrl, apiToken, intervalSeconds, useOriginalTime, verboseOutput); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); @@ -119,44 +127,44 @@ public class GpxSender { 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, double intervalSeconds, boolean useOriginalTime) throws Exception { + private static void sendTrackPoints(List trackPoints, String reittiUrl, String apiToken, double intervalSeconds, boolean useOriginalTime, boolean verboseOutput) 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 timestamp based on mode Instant adjustedTime; if (useOriginalTime) { @@ -170,14 +178,13 @@ public class GpxSender { } else if (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.plusMillis((long) (i * intervalSeconds * 1000)); } else { // Fallback: distribute points evenly from start time adjustedTime = startTime.plusMillis((long) (i * intervalSeconds * 1000)); } } - + // Create Owntracks message OwntracksMessage message = new OwntracksMessage( point.latitude, @@ -185,41 +192,69 @@ public class GpxSender { 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()); - + + System.out.printf("Sending point %d/%d: lat=%.6f, lon=%.6f, time=%s%n", + i + 1, trackPoints.size(), point.latitude, point.longitude, adjustedTime); + try { httpClient.execute(post, response -> { int statusCode = response.getCode(); if (statusCode >= 200 && statusCode < 300) { System.out.println("✓ Sent successfully"); + // Try to read and pretty-print the response body if it exists and verbose is enabled + if (verboseOutput) { + try { + String responseBody = new String(response.getEntity().getContent().readAllBytes()); + if (!responseBody.isEmpty()) { + Object jsonResponse = objectMapper.readValue(responseBody, Object.class); + String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonResponse); + System.out.println("Response:"); + System.out.println(prettyJson); + } + } catch (Exception e) { + // If we can't parse as JSON, just print the raw response + try { + String rawResponse = new String(response.getEntity().getContent().readAllBytes()); + if (!rawResponse.isEmpty()) { + System.out.println("Response: " + rawResponse); + } + } catch (Exception ex) { + // Ignore if we can't read the response + } + } + } } else { System.err.println("✗ Failed with status: " + statusCode); + // Print error response if available + try { + String errorBody = new String(response.getEntity().getContent().readAllBytes()); + System.err.println("Error response: " + errorBody); + } catch (Exception e) { + // Ignore if we can't read the error body + } } 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((long) (intervalSeconds * 1000)); } } } - + System.out.println("Finished sending all track points"); } } diff --git a/src/main/java/com/dedicatedcode/reitti/controller/ReittiIntegrationController.java b/src/main/java/com/dedicatedcode/reitti/controller/ReittiIntegrationController.java index 2a379487..76082b80 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/ReittiIntegrationController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/ReittiIntegrationController.java @@ -2,9 +2,7 @@ package com.dedicatedcode.reitti.controller; import com.dedicatedcode.reitti.model.security.User; import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.*; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -15,36 +13,21 @@ import java.util.concurrent.TimeUnit; @Controller @RequestMapping("/reitti-integration") public class ReittiIntegrationController { - private final JdbcTemplate jdbcTemplate; private final ReittiIntegrationService reittiIntegrationService; - public ReittiIntegrationController(JdbcTemplate jdbcTemplate, ReittiIntegrationService reittiIntegrationService) { - this.jdbcTemplate = jdbcTemplate; + public ReittiIntegrationController(ReittiIntegrationService reittiIntegrationService) { this.reittiIntegrationService = reittiIntegrationService; } @GetMapping("/avatar/{integrationId}") - public ResponseEntity getAvatar(@PathVariable Long integrationId) { - - Map result; - try { - result = jdbcTemplate.queryForMap( - "SELECT mime_type, binary_data FROM remote_user_info WHERE integration_id = ?", - integrationId - ); - } catch (EmptyResultDataAccessException ignored) { - return ResponseEntity.notFound().build(); - } - - String contentType = (String) result.get("mime_type"); - byte[] imageData = (byte[]) result.get("binary_data"); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType(contentType)); - headers.setContentLength(imageData.length); - headers.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)); - - return new ResponseEntity<>(imageData, headers, HttpStatus.OK); + public ResponseEntity getAvatar(@AuthenticationPrincipal User user, @PathVariable Long integrationId) { + return this.reittiIntegrationService.getAvatar(user, integrationId).map(avatarData -> { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(avatarData.mimeType())); + headers.setContentLength(avatarData.imageData().length); + headers.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)); + return new ResponseEntity<>(avatarData.imageData(), headers, HttpStatus.OK); + }).orElse(ResponseEntity.notFound().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)).build()); } diff --git a/src/main/java/com/dedicatedcode/reitti/controller/api/ingestion/owntracks/OwntracksIngestionApiController.java b/src/main/java/com/dedicatedcode/reitti/controller/api/ingestion/owntracks/OwntracksIngestionApiController.java index 9fbf21c0..eaa04ea4 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/api/ingestion/owntracks/OwntracksIngestionApiController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/api/ingestion/owntracks/OwntracksIngestionApiController.java @@ -28,6 +28,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -135,8 +137,15 @@ public class OwntracksIngestionApiController { try { ReittiRemoteInfo info = reittiIntegrationService.getInfo(integration); String tid = generateTid(info.userInfo().username()); - friendsData.add(new OwntracksFriendResponse(tid, info.userInfo().displayName(), null, null)); - } catch (RequestFailedException | RequestTemporaryFailedException e) { + + OwntracksFriendResponse owntracksFriendResponse = reittiIntegrationService.getAvatar(user, integration.getId()) + .map(avatarData -> new OwntracksFriendResponse(tid, info.userInfo().displayName(), avatarData.imageData(), avatarData.mimeType())) + .orElse(new OwntracksFriendResponse(tid, info.userInfo().displayName(), null, null)); + + friendsData.add(owntracksFriendResponse); + Optional latestLocation = reittiIntegrationService.findLatest(user, integration.getId()); + latestLocation.ifPresent(location -> friendsData.add(new OwntracksFriendResponse(tid, info.userInfo().displayName(), location.getLatitude(), location.getLongitude(), Instant.parse(location.getTimestamp()).getEpochSecond()))); + } catch (RequestFailedException | RequestTemporaryFailedException e) { logger.warn("Couldn't fetch info for integration {}", integration.getId(), e); } } 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 15a3b77e..38a3ee65 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/integration/ReittiIntegrationService.java @@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service.integration; import com.dedicatedcode.reitti.dto.*; import com.dedicatedcode.reitti.model.geo.GeoPoint; +import com.dedicatedcode.reitti.model.geo.RawLocationPoint; import com.dedicatedcode.reitti.model.geo.SignificantPlace; import com.dedicatedcode.reitti.model.integration.ReittiIntegration; import com.dedicatedcode.reitti.model.security.RemoteUser; @@ -15,11 +16,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.*; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import javax.xml.stream.Location; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -37,50 +41,24 @@ public class ReittiIntegrationService { private final String advertiseUri; private final ReittiIntegrationJdbcService jdbcService; + private final JdbcTemplate jdbcTemplate; private final RestTemplate restTemplate; private final AvatarService avatarService; private final Map integrationSubscriptions = new ConcurrentHashMap<>(); private final Map userForSubscriptions = new ConcurrentHashMap<>(); - public ReittiIntegrationService(@Value("${reitti.server.advertise-uri}") String advertiseUri, ReittiIntegrationJdbcService jdbcService, + public ReittiIntegrationService(@Value("${reitti.server.advertise-uri}") String advertiseUri, + ReittiIntegrationJdbcService jdbcService, + JdbcTemplate jdbcTemplate, RestTemplate restTemplate, AvatarService avatarService) { this.advertiseUri = advertiseUri; this.jdbcService = jdbcService; + this.jdbcTemplate = jdbcTemplate; this.restTemplate = restTemplate; this.avatarService = avatarService; } - public List getTimelineData(User user, LocalDate selectedDate, ZoneId userTimezone) { - return this.jdbcService - .findAllByUser(user) - .stream().filter(integration -> integration.isEnabled() && VALID_INTEGRATION_STATUS.contains(integration.getStatus())) - .map(integration -> { - - log.debug("Fetching user timeline data for [{}]", integration); - try { - RemoteUser remoteUser = handleRemoteUser(integration); - List timelineEntries = loadTimeLineEntries(integration, selectedDate, userTimezone); - integration = update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now())); - return new UserTimelineData("remote:" + integration.getId(), - remoteUser.getDisplayName(), - this.avatarService.generateInitials(remoteUser.getDisplayName()), - "/reitti-integration/avatar/" + integration.getId(), - integration.getColor(), - timelineEntries, - String.format("/reitti-integration/raw-location-points/%d?date=%s&timezone=%s", integration.getId(), selectedDate, userTimezone), - String.format("/reitti-integration/visits/%d?date=%s&timezone=%s", integration.getId(), selectedDate, userTimezone)); - } catch (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())); - } - return null; - }).toList(); - } - public List getTimelineDataRange(User user, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) { return this.jdbcService .findAllByUser(user) @@ -146,6 +124,24 @@ public class ReittiIntegrationService { } } + public Optional getAvatar(User user, Long integrationId) { + Map result; + try { + result = jdbcTemplate.queryForMap( + "SELECT mime_type, binary_data FROM remote_user_info WHERE integration_id = ?", + integrationId + ); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + + String contentType = (String) result.get("mime_type"); + byte[] imageData = (byte[]) result.get("binary_data"); + + return Optional.of(new AvatarService.AvatarData(contentType,imageData, -1)); + + } + public ProcessedVisitResponse getVisits(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) { return this.jdbcService .findByIdAndUser(integrationId,user) @@ -192,6 +188,51 @@ public class ReittiIntegrationService { .filter(Objects::nonNull) .findFirst().orElse(null); } + + public Optional findLatest(User user, Long integrationId) { + return this.jdbcService + .findByIdAndUser(integrationId, user) + .filter(integration -> integration.isEnabled() && VALID_INTEGRATION_STATUS.contains(integration.getStatus())) + .map(integration -> { + + log.debug("Fetching latest location ata for [{}]", integration); + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-API-TOKEN", integration.getToken()); + HttpEntity entity = new HttpEntity<>(headers); + + String rawLocationDataUrl = integration.getUrl().endsWith("/") ? + integration.getUrl() + "api/v1/latest-location" : + integration.getUrl() + "/api/v1/latest-location"; + ResponseEntity remoteResponse = restTemplate.exchange( + rawLocationDataUrl, + HttpMethod.GET, + entity, + Map.class + ); + + if (remoteResponse.getStatusCode().is2xxSuccessful() && remoteResponse.getStatusCode().is2xxSuccessful() && remoteResponse.getBody() != null && remoteResponse.getBody().containsKey("hasLocation")) { + update(integration.withStatus(ReittiIntegration.Status.ACTIVE).withLastUsed(LocalDateTime.now())); + if (!remoteResponse.getBody().get("hasLocation").equals(true)) { + return null; + } else { + return parseLocationPoint(remoteResponse.getBody().get("point")); + } + } else if (remoteResponse.getStatusCode().is4xxClientError()) { + throw new RequestFailedException(rawLocationDataUrl, remoteResponse.getStatusCode(), remoteResponse.getBody()); + } else { + throw new RequestTemporaryFailedException(rawLocationDataUrl, remoteResponse.getStatusCode(), remoteResponse.getBody()); + } + } catch (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())); + } + return null; + }); + } public List getRawLocationData(User user, Long integrationId, String startDate, String endDate, Integer zoom, String timezone) { return this.jdbcService .findByIdAndUser(integrationId,user) @@ -249,36 +290,6 @@ public class ReittiIntegrationService { return integration; } - private List loadTimeLineEntries(ReittiIntegration integration, LocalDate selectedDate, ZoneId userTimezone) throws RequestFailedException, RequestTemporaryFailedException { - - HttpHeaders headers = new HttpHeaders(); - headers.set("X-API-TOKEN", integration.getToken()); - HttpEntity entity = new HttpEntity<>(headers); - - String timelineUrl = integration.getUrl().endsWith("/") ? - integration.getUrl() + "api/v1/reitti-integration/timeline?date={date}&timezone={timezone}" : - integration.getUrl() + "/api/v1/reitti-integration/timeline?date={date}&timezone={timezone}"; - - - ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {}; - ResponseEntity> remoteResponse = restTemplate.exchange( - timelineUrl, - HttpMethod.GET, - entity, - typeRef, - selectedDate, - userTimezone.getId() - ); - - if (remoteResponse.getStatusCode().is2xxSuccessful()) { - return remoteResponse.getBody(); - } else if (remoteResponse.getStatusCode().is4xxClientError()) { - throw new RequestFailedException(timelineUrl, remoteResponse.getStatusCode(), remoteResponse.getBody()); - } else { - throw new RequestTemporaryFailedException(timelineUrl, remoteResponse.getStatusCode(), remoteResponse.getBody()); - } - } - private List loadTimeLineEntriesRange(ReittiIntegration integration, LocalDate startDate, LocalDate endDate, ZoneId userTimezone) throws RequestFailedException, RequestTemporaryFailedException { HttpHeaders headers = new HttpHeaders(); @@ -289,7 +300,6 @@ public class ReittiIntegrationService { integration.getUrl() + "api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}" : integration.getUrl() + "/api/v1/reitti-integration/timeline?startDate={startDate}&endDate={endDate}&timezone={timezone}"; - ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {}; ResponseEntity> remoteResponse = restTemplate.exchange( timelineUrl, @@ -353,9 +363,9 @@ public class ReittiIntegrationService { 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); @@ -383,7 +393,7 @@ public class ReittiIntegrationService { 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); @@ -391,11 +401,11 @@ public class ReittiIntegrationService { 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, @@ -403,7 +413,7 @@ public class ReittiIntegrationService { entity, SubscriptionResponse.class ); - + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { log.debug("Successfully subscribed to integration: [{}]", integration.getId()); synchronized (integrationSubscriptions) { @@ -482,14 +492,14 @@ public class ReittiIntegrationService { if (placesData == null) { return new ProcessedVisitResponse(Collections.emptyList()); } - + List places = placesData.stream() .map(this::parsePlaceVisitSummary) .toList(); - + return new ProcessedVisitResponse(places); } - + @SuppressWarnings("unchecked") private ProcessedVisitResponse.PlaceVisitSummary parsePlaceVisitSummary(Map placeData) { // Parse place info @@ -507,7 +517,7 @@ public class ReittiIntegrationService { SignificantPlace.PlaceType.valueOf(placeInfo.get("type").toString()), polygon ); - + // Parse visits List> visitsData = (List>) placeData.get("visits"); List visits = visitsData.stream() @@ -518,12 +528,12 @@ public class ReittiIntegrationService { getLongValue(visitData, "durationSeconds") )) .toList(); - + // Parse summary data long totalDurationMs = getLongValue(placeData, "totalDurationMs"); // Convert to milliseconds int visitCount = getIntValue(placeData, "visitCount"); String color = "#3388ff"; // Default color, could be extracted from response if available - + return new ProcessedVisitResponse.PlaceVisitSummary(place, visits, totalDurationMs, visitCount, color); } @@ -543,7 +553,7 @@ public class ReittiIntegrationService { } return polygon; } - + private Long getLongValue(Map map, String key) { Object value = map.get(key); if (value instanceof Number) { @@ -551,7 +561,7 @@ public class ReittiIntegrationService { } return null; } - + private Double getDoubleValue(Map map, String key) { Object value = map.get(key); if (value instanceof Number) { @@ -559,7 +569,7 @@ public class ReittiIntegrationService { } return null; } - + private Integer getIntValue(Map map, String key) { Object value = map.get(key); if (value instanceof Number) { @@ -568,4 +578,24 @@ public class ReittiIntegrationService { return 0; } + private LocationPoint parseLocationPoint(Object locationObj) { + if (locationObj == null) { + return null; + } + + Map locationMap = (Map) locationObj; + + LocationPoint locationPoint = new LocationPoint(); + locationPoint.setLatitude(getDoubleValue(locationMap, "latitude")); + locationPoint.setLongitude(getDoubleValue(locationMap, "longitude")); + locationPoint.setTimestamp((String) locationMap.get("timestamp")); + locationPoint.setAccuracyMeters(getDoubleValue(locationMap, "accuracyMeters")); + + if (locationMap.containsKey("elevationMeters")) { + locationPoint.setElevationMeters(getDoubleValue(locationMap, "elevationMeters")); + } + + return locationPoint; + } + }