From be7018bf4c0c427296527f807d7e7f95bd0dd1c7 Mon Sep 17 00:00:00 2001 From: Daniel Graf Date: Tue, 8 Jul 2025 16:07:29 +0200 Subject: [PATCH] 88 support the new google location historyjson (#100) --- README.md | 13 +- docs/tools/GoogleTimelineRandomizer.java | 203 +- .../controller/FileImportController.java | 293 + .../reitti/controller/SettingsController.java | 226 +- .../api/ImportDataApiController.java | 156 - .../reitti/dto/LocationDataRequest.java | 11 + .../reitti/event/LocationProcessEvent.java | 13 + .../repository/ProcessedVisitJdbcService.java | 4 +- .../reitti/service/ImportHandler.java | 48 - .../reitti/service/ImportStateHolder.java | 22 + .../DefaultGeocodeServiceManager.java | 1 - .../importer/BaseGoogleTimelineImporter.java | 127 + .../service/importer/GeoJsonImporter.java | 8 +- .../GoogleAndroidTimelineImporter.java | 101 + .../importer/GoogleIOSTimelineImporter.java | 97 + .../importer/GoogleRecordsImporter.java | 8 +- .../importer/GoogleTimelineImporter.java | 210 - .../reitti/service/importer/GpxImporter.java | 10 +- .../importer/ImportBatchProcessor.java | 3 +- .../service/importer/dto/ios/IOSActivity.java | 41 + .../importer/dto/ios/IOSSemanticSegment.java | 87 + .../importer/dto/ios/IOSTopCandidate.java | 61 + .../service/importer/dto/ios/IOSVisit.java | 39 + .../importer/dto/ios/TimelinePathPoint.java | 28 + .../RawLocationPointProcessingTrigger.java | 60 +- .../processing/TripDetectionService.java | 3 - .../processing/VisitDetectionService.java | 98 +- .../processing/VisitMergingService.java | 23 +- src/main/resources/application-dev.properties | 2 +- src/main/resources/messages.properties | 6 + src/main/resources/messages_de.properties | 8 +- src/main/resources/messages_fi.properties | 6 + src/main/resources/messages_fr.properties | 6 + src/main/resources/static/css/main.css | 31 + .../templates/fragments/file-upload.html | 97 +- src/main/resources/templates/settings.html | 61 + .../dedicatedcode/reitti/TestingService.java | 14 +- .../BaseGoogleTimelineImporterTest.java | 67 + .../GoogleAndroidTimelineImporterTest.java | 47 + ...ava => GoogleIOSTimelineImporterTest.java} | 21 +- .../importer/GoogleRecordsImporterTest.java | 3 +- .../resources/application-test.properties | 2 +- src/test/resources/data/google/Records.json | 3 +- src/test/resources/data/google/Zeitachse.json | 8238 ----------------- ... => timeline_from_android_randomized.json} | 0 .../google/timeline_from_ios_randomized.json | 627 ++ 46 files changed, 2148 insertions(+), 9085 deletions(-) create mode 100644 src/main/java/com/dedicatedcode/reitti/controller/FileImportController.java delete mode 100644 src/main/java/com/dedicatedcode/reitti/controller/api/ImportDataApiController.java delete mode 100644 src/main/java/com/dedicatedcode/reitti/service/ImportHandler.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/ImportStateHolder.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporter.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporter.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporter.java delete mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporter.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSActivity.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSSemanticSegment.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSTopCandidate.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSVisit.java create mode 100644 src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/TimelinePathPoint.java create mode 100644 src/test/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporterTest.java create mode 100644 src/test/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporterTest.java rename src/test/java/com/dedicatedcode/reitti/service/importer/{GoogleTimelineImporterTest.java => GoogleIOSTimelineImporterTest.java} (67%) delete mode 100644 src/test/resources/data/google/Zeitachse.json rename src/test/resources/data/google/{tl_randomized.json => timeline_from_android_randomized.json} (100%) create mode 100644 src/test/resources/data/google/timeline_from_ios_randomized.json diff --git a/README.md b/README.md index 25a82c8d..c45332d0 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ The easiest way to get started is using Docker Compose: 3. Access the application at `http://localhost:8080` -4. Create your first user account through the web interface +4. Login with admin:admin ### Development Setup @@ -98,7 +98,7 @@ For development or custom deployments: 3. Access the application at `http://localhost:8080` -Default user name and password is `admin` +Default username and password is `admin` ### Building Docker Image @@ -114,11 +114,10 @@ docker build -t reitti/reitti:latest . After starting the application: -1. **Create User Account**: Set up your first user account -2. **Generate API Token**: Create an API token in Settings → API Tokens for mobile app integration -3. **Configure Geocoding**: Add geocoding services in Settings → Geocoding for address resolution -4. **Import Data**: Upload your location data via Settings → Import Data -5. **Set up Mobile Apps**: Configure OwnTracks or GPSLogger for real-time tracking +1. **Generate API Token**: Create an API token in Settings → API Tokens for mobile app integration +2. **Configure Geocoding**: Add geocoding services in Settings → Geocoding for address resolution +3. **Import Data**: Upload your location data via Settings → Import Data +4. **Set up Mobile Apps**: Configure OwnTracks or GPSLogger for real-time tracking ## Docker Deployment diff --git a/docs/tools/GoogleTimelineRandomizer.java b/docs/tools/GoogleTimelineRandomizer.java index 19539dec..a135c003 100644 --- a/docs/tools/GoogleTimelineRandomizer.java +++ b/docs/tools/GoogleTimelineRandomizer.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.FileReader; import java.io.IOException; -import java.lang.reflect.Array; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -21,12 +20,13 @@ public class GoogleTimelineRandomizer { public static void main(String[] args) { if (args.length == 0) { - System.err.println("Usage: GoogleTimelineRandomizer --file= --output-dir= [--max-semantic-segments=] [--max-raw-signals=]"); + System.err.println("Usage: GoogleTimelineRandomizer --file= --output-dir= --mode= [--max-semantic-segments=] [--max-raw-signals=]"); System.exit(1); } String filePath = null; String outputDir = null; + String mode = null; Integer maxSemanticSegments = null; Integer maxRawSignals = null; @@ -36,6 +36,8 @@ public class GoogleTimelineRandomizer { filePath = arg.substring("--file=".length()); } else if (arg.startsWith("--output-dir=")) { outputDir = arg.substring("--output-dir=".length()); + } else if (arg.startsWith("--mode=")) { + mode = arg.substring("--mode=".length()); } else if (arg.startsWith("--max-semantic-segments=")) { try { maxSemanticSegments = Integer.parseInt(arg.substring("--max-semantic-segments=".length())); @@ -52,15 +54,21 @@ public class GoogleTimelineRandomizer { } } else { System.err.println("Unknown argument: " + arg); - System.err.println("Usage: GoogleTimelineRandomizer --file-path= --output-dir= [--max-semantic-segments=] [--max-raw-signals=]"); + System.err.println("Usage: GoogleTimelineRandomizer --file= --output-dir= --mode= [--max-semantic-segments=] [--max-raw-signals=]"); System.exit(1); } } // Validate required arguments - if (filePath == null || outputDir == null) { - System.err.println("Missing required arguments: --file-path and --output-dir are required"); - System.err.println("Usage: GoogleTimelineRandomizer --file-path= --output-dir= [--max-semantic-segments=] [--max-raw-signals=]"); + if (filePath == null || outputDir == null || mode == null) { + System.err.println("Missing required arguments: --file, --output-dir and --mode are required"); + System.err.println("Usage: GoogleTimelineRandomizer --file= --output-dir= --mode= [--max-semantic-segments=] [--max-raw-signals=]"); + System.exit(1); + } + + // Validate mode argument + if (!mode.equals("android") && !mode.equals("ios")) { + System.err.println("Invalid mode value: " + mode + ". Must be 'android' or 'ios'"); System.exit(1); } @@ -70,14 +78,14 @@ public class GoogleTimelineRandomizer { double longitudeAdjustment = random.nextDouble(-20, 20); try { - load(filePath, outputDir, timeAdjustments, longitudeAdjustment, maxSemanticSegments, maxRawSignals); + load(filePath, outputDir, mode, timeAdjustments, longitudeAdjustment, maxSemanticSegments, maxRawSignals); } catch (IOException e) { System.err.println("Error loading file: " + e.getMessage()); System.exit(1); } } - private static void load(String filePath, String outputDir, int timeAdjustmentInMinutes, double longitudeAdjustment, Integer maxSemanticSegments, Integer maxRawSignals) throws IOException { + private static void load(String filePath, String outputDir, String mode, int timeAdjustmentInMinutes, double longitudeAdjustment, Integer maxSemanticSegments, Integer maxRawSignals) throws IOException { Path path = Paths.get(filePath); if (!Files.exists(path)) { @@ -89,7 +97,91 @@ public class GoogleTimelineRandomizer { } ObjectMapper mapper = new ObjectMapper(); - ObjectNode root = (ObjectNode) mapper.readTree(new FileReader(filePath)); + JsonNode root; + if (mode.equals("android")) { + root = modifyAndroidFile((ObjectNode) mapper.readTree(new FileReader(filePath)), timeAdjustmentInMinutes, longitudeAdjustment, maxSemanticSegments, maxRawSignals); + } else if (mode.equals("ios")) { + ArrayNode semanticSegments = (ArrayNode) mapper.readTree(new FileReader(filePath)); + if (maxSemanticSegments == null) { + root = modifyIosFile(semanticSegments, timeAdjustmentInMinutes, longitudeAdjustment); + } else { + ArrayNode newArray = mapper.createArrayNode(); + for (int i = 0; i < maxSemanticSegments; i++) { + newArray.add(semanticSegments.get(i)); + } + root = modifyIosFile(newArray, timeAdjustmentInMinutes, longitudeAdjustment); + } + } else { + throw new IOException("Invalid mode value: " + mode); + } + + // Create output filename by appending date before extension + Path inputPath = Paths.get(filePath); + String inputFileName = inputPath.getFileName().toString(); + String nameWithoutExtension; + String extension; + + int lastDotIndex = inputFileName.lastIndexOf('.'); + if (lastDotIndex > 0) { + nameWithoutExtension = inputFileName.substring(0, lastDotIndex); + extension = inputFileName.substring(lastDotIndex); + } else { + nameWithoutExtension = inputFileName; + extension = ""; + } + + String outputFileName = nameWithoutExtension + "_randomized" + extension; + Path outputPath = Paths.get(outputDir, outputFileName); + + // Ensure output directory exists + Files.createDirectories(Paths.get(outputDir)); + + // Write randomized JSON to output file + mapper.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), root); + + System.out.println("Filtered data written to: " + outputPath); + } + + private static JsonNode modifyIosFile(ArrayNode semanticSegments, int timeAdjustmentInMinutes, double longitudeAdjustment) { + for (JsonNode semanticSegment : semanticSegments) { + ObjectNode current = (ObjectNode) semanticSegment; + if (current.has("endTime")) { + adjustTime(timeAdjustmentInMinutes, current, "endTime"); + } + if (current.has("startTime")) { + adjustTime(timeAdjustmentInMinutes, current, "startTime"); + } + + if (current.has("activity")) { + ObjectNode activity = (ObjectNode) current.get("activity"); + if (activity.has("start")) { + adjustGeoPoint(longitudeAdjustment, activity, "start"); + } + if (activity.has("end")) { + adjustGeoPoint(longitudeAdjustment, activity, "end"); + } + } + if (current.has("visit")) { + ObjectNode visit = (ObjectNode) current.get("visit"); + if (visit.has("topCandidate") && visit.path("topCandidate").has("placeLocation")) { + adjustGeoPoint(longitudeAdjustment, (ObjectNode) visit.get("topCandidate"), "placeLocation"); + } + } + if (current.has("timelinePath")) { + ArrayNode timelinePath = (ArrayNode) current.get("timelinePath"); + for (JsonNode jsonNode : timelinePath) { + if (jsonNode.has("point")) { + adjustGeoPoint(longitudeAdjustment, (ObjectNode) jsonNode, "point"); + } + } + } + } + + return semanticSegments; + } + + private static JsonNode modifyAndroidFile(ObjectNode root, int timeAdjustmentInMinutes, double longitudeAdjustment, Integer maxSemanticSegments, Integer maxRawSignals) { + ArrayNode semanticSegments = (ArrayNode) root.path("semanticSegments"); ArrayNode rawSignals = (ArrayNode) root.path("rawSignals"); @@ -116,20 +208,21 @@ public class GoogleTimelineRandomizer { rawSignals = limitedRawSignals; } + for (JsonNode segment : semanticSegments) { ObjectNode current = (ObjectNode) segment; if (current.has("startTime")) { - adjustTime(timeAdjustmentInMinutes, current.get("startTime"), current, "startTime"); + adjustTime(timeAdjustmentInMinutes, current, "startTime"); } if (current.has("endTime")) { - adjustTime(timeAdjustmentInMinutes, current.get("endTime"), current, "endTime"); + adjustTime(timeAdjustmentInMinutes, current, "endTime"); } if (current.has("timelinePath")) { ArrayNode timelinePath = (ArrayNode) current.get("timelinePath"); for (JsonNode jsonNode : timelinePath) { if (jsonNode.isObject()) { if (jsonNode.has("time")) { - adjustTime(timeAdjustmentInMinutes, jsonNode.get("time"), (ObjectNode) jsonNode, "time"); + adjustTime(timeAdjustmentInMinutes, (ObjectNode) jsonNode, "time"); } if (jsonNode.has("point")) { adjustPoint(longitudeAdjustment, jsonNode.get("point"), (ObjectNode) jsonNode, "point"); @@ -167,49 +260,59 @@ public class GoogleTimelineRandomizer { } } - for (JsonNode rawSignal : rawSignals) { - ObjectNode current = (ObjectNode) rawSignal; - if (current.has("position")) { - ObjectNode position = (ObjectNode) current.get("position"); - if (position.has("LatLng")) { - adjustPoint(longitudeAdjustment, position.get("LatLng"), position, "LatLng"); + if (rawSignals != null) { + for (JsonNode rawSignal : rawSignals) { + ObjectNode current = (ObjectNode) rawSignal; + if (current.has("position")) { + ObjectNode position = (ObjectNode) current.get("position"); + if (position.has("LatLng")) { + adjustPoint(longitudeAdjustment, position.get("LatLng"), position, "LatLng"); + } + if (position.has("timestamp")) { + adjustTime(timeAdjustmentInMinutes, position, "timestamp"); + } } - if (position.has("timestamp")) { - adjustTime(timeAdjustmentInMinutes, position.get("timestamp"), position, "timestamp"); + + if (current.has("wifiScan")) { + current.set("wifiScan", new ObjectNode(JsonNodeFactory.instance)); } } - - if (current.has("wifiScan")) { - current.set("wifiScan", new ObjectNode(JsonNodeFactory.instance)); - } - } - // Create output filename by appending date before extension - Path inputPath = Paths.get(filePath); - String inputFileName = inputPath.getFileName().toString(); - String nameWithoutExtension; - String extension; - - int lastDotIndex = inputFileName.lastIndexOf('.'); - if (lastDotIndex > 0) { - nameWithoutExtension = inputFileName.substring(0, lastDotIndex); - extension = inputFileName.substring(lastDotIndex); - } else { - nameWithoutExtension = inputFileName; - extension = ""; } - String outputFileName = nameWithoutExtension + "_randomized" + extension; - Path outputPath = Paths.get(outputDir, outputFileName); - - // Ensure output directory exists - Files.createDirectories(Paths.get(outputDir)); - - // Write randomized JSON to output file - mapper.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), root); - - System.out.println("Filtered data written to: " + outputPath); + return root; } + private static void adjustGeoPoint(double longitudeAdjustment, ObjectNode jsonNode, String name) { + String pointText = jsonNode.get(name).asText(); + + // Parse the point text in format "geo:55.605487,13.007670" + if (pointText.startsWith("geo:")) { + String coordinates = pointText.substring(4); // Remove "geo:" prefix + String[] parts = coordinates.split(","); + + if (parts.length == 2) { + try { + double latitude = Double.parseDouble(parts[0].trim()); + double longitude = Double.parseDouble(parts[1].trim()); + + // Apply longitude adjustment + double adjustedLongitude = longitude + longitudeAdjustment; + + // Format back to the original geo: format + String adjustedPoint = String.format("geo:%.7f,%.7f", latitude, adjustedLongitude); + + // Add the adjusted value to the JSON node + jsonNode.put(name, adjustedPoint); + } catch (NumberFormatException e) { + System.err.println("Failed to parse geo coordinates: " + pointText); + } + } else { + System.err.println("Invalid geo format: " + pointText); + } + } else { + System.err.println("Point text does not start with 'geo:': " + pointText); + } + } private static void adjustPoint(double longitudeAdjustment, JsonNode point, ObjectNode jsonNode, String name) { String pointText = point.asText(); @@ -239,8 +342,8 @@ public class GoogleTimelineRandomizer { } } - private static void adjustTime(int timeAdjustmentInMinutes, JsonNode jsonNode, ObjectNode current, String name) { - String currentValue = jsonNode.asText(); + private static void adjustTime(int timeAdjustmentInMinutes, ObjectNode current, String name) { + String currentValue = current.get(name).asText(); ZonedDateTime currentTime = ZonedDateTime.parse(currentValue); long newTime = currentTime.toEpochSecond() + (timeAdjustmentInMinutes * 60L); current.put(name, ZonedDateTime.ofInstant(Instant.ofEpochSecond(newTime), currentTime.getZone()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); diff --git a/src/main/java/com/dedicatedcode/reitti/controller/FileImportController.java b/src/main/java/com/dedicatedcode/reitti/controller/FileImportController.java new file mode 100644 index 00000000..defc39db --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/controller/FileImportController.java @@ -0,0 +1,293 @@ +package com.dedicatedcode.reitti.controller; + +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.importer.*; +import com.dedicatedcode.reitti.service.processing.RawLocationPointProcessingTrigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +@Controller +@RequestMapping("/import") +public class FileImportController { + + private static final Logger logger = LoggerFactory.getLogger(FileImportController.class); + + private final RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger; + private final GpxImporter gpxImporter; + private final GoogleRecordsImporter googleRecordsImporter; + private final GoogleAndroidTimelineImporter googleAndroidTimelineImporter; + private final GoogleIOSTimelineImporter googleTimelineIOSImporter; + private final GeoJsonImporter geoJsonImporter; + + public FileImportController(RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger, + GpxImporter gpxImporter, + GoogleRecordsImporter googleRecordsImporter, + GoogleAndroidTimelineImporter googleAndroidTimelineImporter, + GoogleIOSTimelineImporter googleTimelineIOSImporter, GeoJsonImporter geoJsonImporter) { + this.rawLocationPointProcessingTrigger = rawLocationPointProcessingTrigger; + this.gpxImporter = gpxImporter; + this.googleRecordsImporter = googleRecordsImporter; + this.googleAndroidTimelineImporter = googleAndroidTimelineImporter; + this.googleTimelineIOSImporter = googleTimelineIOSImporter; + this.geoJsonImporter = geoJsonImporter; + } + + @GetMapping("/file-upload-content") + public String getDataImportContent() { + return "fragments/file-upload :: file-upload-content"; + } + + @PostMapping("/gpx") + public String importGpx(@RequestParam("files") MultipartFile[] files, + Authentication authentication, + Model model) { + User user = (User) authentication.getPrincipal(); + + if (files.length == 0) { + model.addAttribute("uploadErrorMessage", "No files selected"); + return "fragments/file-upload :: file-upload-content"; + } + + int totalProcessed = 0; + int successCount = 0; + StringBuilder errorMessages = new StringBuilder(); + + for (MultipartFile file : files) { + if (file.isEmpty() || file.getOriginalFilename() == null) { + errorMessages.append("File ").append(file.getOriginalFilename()).append(" is empty. "); + continue; + } + + if (!file.getOriginalFilename().endsWith(".gpx")) { + errorMessages.append("File ").append(file.getOriginalFilename()).append(" is not a GPX file. "); + continue; + } + + try (InputStream inputStream = file.getInputStream()) { + Map result = this.gpxImporter.importGpx(inputStream, user); + + if ((Boolean) result.get("success")) { + totalProcessed += (Integer) result.get("pointsReceived"); + successCount++; + } else { + errorMessages.append("Error processing ").append(file.getOriginalFilename()).append(": ") + .append(result.get("error")).append(". "); + } + } catch (IOException e) { + errorMessages.append("Error processing ").append(file.getOriginalFilename()).append(": ") + .append(e.getMessage()).append(". "); + } + } + + if (successCount > 0) { + String message = "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"; + if (!errorMessages.isEmpty()) { + message += ". Errors: " + errorMessages; + } + model.addAttribute("uploadSuccessMessage", message); + + // Trigger processing pipeline for imported data + try { + rawLocationPointProcessingTrigger.start(); + } catch (Exception e) { + logger.warn("Failed to trigger processing pipeline after GPX import", e); + } + } else { + model.addAttribute("uploadErrorMessage", "No files were processed successfully. " + errorMessages); + } + + return "fragments/file-upload :: file-upload-content"; + } + + @PostMapping("/google-records") + public String importGoogleRecords(@RequestParam("file") MultipartFile file, + Authentication authentication, + Model model) { + User user = (User) authentication.getPrincipal(); + + if (file.isEmpty() || file.getOriginalFilename() == null) { + model.addAttribute("uploadErrorMessage", "File is empty"); + return "fragments/file-upload :: file-upload-content"; + } + + if (!file.getOriginalFilename().endsWith(".json")) { + model.addAttribute("uploadErrorMessage", "Only JSON files are supported"); + return "fragments/file-upload :: file-upload-content"; + } + + try (InputStream inputStream = file.getInputStream()) { + Map result = this.googleRecordsImporter.importGoogleRecords(inputStream, user); + + if ((Boolean) result.get("success")) { + model.addAttribute("uploadSuccessMessage", result.get("message")); + + // Trigger processing pipeline for imported data + try { + rawLocationPointProcessingTrigger.start(); + } catch (Exception e) { + logger.warn("Failed to trigger processing pipeline after Google Records import", e); + } + } else { + model.addAttribute("uploadErrorMessage", result.get("error")); + } + + return "fragments/file-upload :: file-upload-content"; + } catch (IOException e) { + model.addAttribute("uploadErrorMessage", "Error processing file: " + e.getMessage()); + return "fragments/file-upload :: file-upload-content"; + } + } + + @PostMapping("/google-timeline-android") + public String importGoogleTimelineAndroid(@RequestParam("file") MultipartFile file, + Authentication authentication, + Model model) { + User user = (User) authentication.getPrincipal(); + + if (file.isEmpty() || file.getOriginalFilename() == null) { + model.addAttribute("uploadErrorMessage", "File is empty"); + return "fragments/file-upload :: file-upload-content"; + } + + if (!file.getOriginalFilename().endsWith(".json")) { + model.addAttribute("uploadErrorMessage", "Only JSON files are supported"); + return "fragments/file-upload :: file-upload-content"; + } + + try (InputStream inputStream = file.getInputStream()) { + Map result = this.googleAndroidTimelineImporter.importTimeline(inputStream, user); + + if ((Boolean) result.get("success")) { + model.addAttribute("uploadSuccessMessage", result.get("message")); + + // Trigger processing pipeline for imported data + try { + rawLocationPointProcessingTrigger.start(); + } catch (Exception e) { + logger.warn("Failed to trigger processing pipeline after Google Timeline Android import", e); + } + } else { + model.addAttribute("uploadErrorMessage", result.get("error")); + } + + return "fragments/file-upload :: file-upload-content"; + } catch (IOException e) { + model.addAttribute("uploadErrorMessage", "Error processing file: " + e.getMessage()); + return "fragments/file-upload :: file-upload-content"; + } + } + + @PostMapping("/google-timeline-ios") + public String importGoogleTimelineIOS(@RequestParam("file") MultipartFile file, + Authentication authentication, + Model model) { + User user = (User) authentication.getPrincipal(); + + if (file.isEmpty() || file.getOriginalFilename() == null) { + model.addAttribute("uploadErrorMessage", "File is empty"); + return "fragments/file-upload :: file-upload-content"; + } + + if (!file.getOriginalFilename().endsWith(".json")) { + model.addAttribute("uploadErrorMessage", "Only JSON files are supported"); + return "fragments/file-upload :: file-upload-content"; + } + + try (InputStream inputStream = file.getInputStream()) { + Map result = this.googleTimelineIOSImporter.importTimeline(inputStream, user); + + if ((Boolean) result.get("success")) { + model.addAttribute("uploadSuccessMessage", result.get("message")); + + // Trigger processing pipeline for imported data + try { + rawLocationPointProcessingTrigger.start(); + } catch (Exception e) { + logger.warn("Failed to trigger processing pipeline after Google Timeline iOS import", e); + } + } else { + model.addAttribute("uploadErrorMessage", result.get("error")); + } + + return "fragments/file-upload :: file-upload-content"; + } catch (IOException e) { + model.addAttribute("uploadErrorMessage", "Error processing file: " + e.getMessage()); + return "fragments/file-upload :: file-upload-content"; + } + } + + @PostMapping("/geojson") + public String importGeoJson(@RequestParam("files") MultipartFile[] files, + Authentication authentication, + Model model) { + User user = (User) authentication.getPrincipal(); + + if (files.length == 0) { + model.addAttribute("uploadErrorMessage", "No files selected"); + return "fragments/file-upload :: file-upload-content"; + } + + int totalProcessed = 0; + int successCount = 0; + StringBuilder errorMessages = new StringBuilder(); + + for (MultipartFile file : files) { + if (file.isEmpty()) { + errorMessages.append("File ").append(file.getOriginalFilename()).append(" is empty. "); + continue; + } + + String filename = file.getOriginalFilename(); + if (filename == null || (!filename.endsWith(".geojson") && !filename.endsWith(".json"))) { + errorMessages.append("File ").append(filename).append(" is not a GeoJSON file. "); + continue; + } + + try (InputStream inputStream = file.getInputStream()) { + Map result = this.geoJsonImporter.importGeoJson(inputStream, user); + + if ((Boolean) result.get("success")) { + totalProcessed += (Integer) result.get("pointsReceived"); + successCount++; + } else { + errorMessages.append("Error processing ").append(filename).append(": ") + .append(result.get("error")).append(". "); + } + } catch (IOException e) { + errorMessages.append("Error processing ").append(filename).append(": ") + .append(e.getMessage()).append(". "); + } + } + + if (successCount > 0) { + String message = "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"; + if (!errorMessages.isEmpty()) { + message += ". Errors: " + errorMessages; + } + model.addAttribute("uploadSuccessMessage", message); + + // Trigger processing pipeline for imported data + try { + rawLocationPointProcessingTrigger.start(); + } catch (Exception e) { + logger.warn("Failed to trigger processing pipeline after GeoJSON import", e); + } + } else { + model.addAttribute("uploadErrorMessage", "No files were processed successfully. " + errorMessages); + } + + return "fragments/file-upload :: file-upload-content"; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/controller/SettingsController.java b/src/main/java/com/dedicatedcode/reitti/controller/SettingsController.java index 2cc4b3c8..04bb111b 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/SettingsController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/SettingsController.java @@ -22,7 +22,6 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.LocaleResolver; @@ -35,15 +34,14 @@ import java.util.stream.Collectors; @Controller @RequestMapping("/settings") public class SettingsController { + private static final Logger logger = LoggerFactory.getLogger(SettingsController.class); private final ApiTokenService apiTokenService; private final UserJdbcService userJdbcService; private final QueueStatsService queueStatsService; private final PlaceService placeService; private final SignificantPlaceJdbcService placeJdbcService; - private final ImportHandler importHandler; private final GeocodeServiceJdbcService geocodeServiceJdbcService; - private final RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger; private final ImmichIntegrationService immichIntegrationService; private final OwnTracksRecorderIntegrationService ownTracksRecorderIntegrationService; private final VisitJdbcService visitJdbcService; @@ -56,15 +54,13 @@ public class SettingsController { private final MessageSource messageSource; private final LocaleResolver localeResolver; private final Properties gitProperties = new Properties(); - private static final Logger logger = LoggerFactory.getLogger(SettingsController.class); + private final RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger; public SettingsController(ApiTokenService apiTokenService, UserJdbcService userJdbcService, QueueStatsService queueStatsService, PlaceService placeService, SignificantPlaceJdbcService placeJdbcService, - ImportHandler importHandler, GeocodeServiceJdbcService geocodeServiceJdbcService, - RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger, ImmichIntegrationService immichIntegrationService, OwnTracksRecorderIntegrationService ownTracksRecorderIntegrationService, VisitJdbcService visitJdbcService, @@ -75,15 +71,14 @@ public class SettingsController { @Value("${reitti.geocoding.max-errors}") int maxErrors, @Value("${reitti.data-management.enabled:false}") boolean dataManagementEnabled, MessageSource messageSource, - LocaleResolver localeResolver) { + LocaleResolver localeResolver, + RawLocationPointProcessingTrigger rawLocationPointProcessingTrigger) { this.apiTokenService = apiTokenService; this.userJdbcService = userJdbcService; this.queueStatsService = queueStatsService; this.placeService = placeService; this.placeJdbcService = placeJdbcService; - this.importHandler = importHandler; this.geocodeServiceJdbcService = geocodeServiceJdbcService; - this.rawLocationPointProcessingTrigger = rawLocationPointProcessingTrigger; this.immichIntegrationService = immichIntegrationService; this.ownTracksRecorderIntegrationService = ownTracksRecorderIntegrationService; this.visitJdbcService = visitJdbcService; @@ -95,6 +90,7 @@ public class SettingsController { this.dataManagementEnabled = dataManagementEnabled; this.messageSource = messageSource; this.localeResolver = localeResolver; + this.rawLocationPointProcessingTrigger = rawLocationPointProcessingTrigger; loadGitProperties(); } @@ -365,7 +361,7 @@ public class SettingsController { @GetMapping("/file-upload-content") public String getDataImportContent() { - return "fragments/file-upload :: file-upload-content"; + return "redirect:/import/file-upload-content"; } @GetMapping("/language-content") @@ -693,209 +689,6 @@ public class SettingsController { return "fragments/settings :: user-form"; } - @PostMapping("/import/gpx") - public String importGpx(@RequestParam("files") MultipartFile[] files, - Authentication authentication, - Model model) { - User user = (User) authentication.getPrincipal(); - - if (files.length == 0) { - model.addAttribute("uploadErrorMessage", "No files selected"); - return "fragments/file-upload :: file-upload-content"; - } - - int totalProcessed = 0; - int successCount = 0; - StringBuilder errorMessages = new StringBuilder(); - - for (MultipartFile file : files) { - if (file.isEmpty() || file.getOriginalFilename() == null) { - errorMessages.append("File ").append(file.getOriginalFilename()).append(" is empty. "); - continue; - } - - if (!file.getOriginalFilename().endsWith(".gpx")) { - errorMessages.append("File ").append(file.getOriginalFilename()).append(" is not a GPX file. "); - continue; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGpx(inputStream, user); - - if ((Boolean) result.get("success")) { - totalProcessed += (Integer) result.get("pointsReceived"); - successCount++; - } else { - errorMessages.append("Error processing ").append(file.getOriginalFilename()).append(": ") - .append(result.get("error")).append(". "); - } - } catch (IOException e) { - errorMessages.append("Error processing ").append(file.getOriginalFilename()).append(": ") - .append(e.getMessage()).append(". "); - } - } - - if (successCount > 0) { - String message = "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"; - if (!errorMessages.isEmpty()) { - message += ". Errors: " + errorMessages; - } - model.addAttribute("uploadSuccessMessage", message); - - // Trigger processing pipeline for imported data - try { - rawLocationPointProcessingTrigger.start(); - } catch (Exception e) { - logger.warn("Failed to trigger processing pipeline after GPX import", e); - } - } else { - model.addAttribute("uploadErrorMessage", "No files were processed successfully. " + errorMessages); - } - - return "fragments/file-upload :: file-upload-content"; - } - - @PostMapping("/import/google-records") - public String importGoogleRecords(@RequestParam("file") MultipartFile file, - Authentication authentication, - Model model) { - User user = (User) authentication.getPrincipal(); - - if (file.isEmpty() || file.getOriginalFilename() == null) { - model.addAttribute("uploadErrorMessage", "File is empty"); - return "fragments/file-upload :: file-upload-content"; - } - - if (!file.getOriginalFilename().endsWith(".json")) { - model.addAttribute("uploadErrorMessage", "Only JSON files are supported"); - return "fragments/file-upload :: file-upload-content"; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGoogleRecords(inputStream, user); - - if ((Boolean) result.get("success")) { - model.addAttribute("uploadSuccessMessage", result.get("message")); - - // Trigger processing pipeline for imported data - try { - rawLocationPointProcessingTrigger.start(); - } catch (Exception e) { - logger.warn("Failed to trigger processing pipeline after Google Records import", e); - } - } else { - model.addAttribute("uploadErrorMessage", result.get("error")); - } - - return "fragments/file-upload :: file-upload-content"; - } catch (IOException e) { - model.addAttribute("uploadErrorMessage", "Error processing file: " + e.getMessage()); - return "fragments/file-upload :: file-upload-content"; - } - } - - @PostMapping("/import/google-timeline") - public String importGoogleTimeline(@RequestParam("file") MultipartFile file, - Authentication authentication, - Model model) { - User user = (User) authentication.getPrincipal(); - - if (file.isEmpty() || file.getOriginalFilename() == null) { - model.addAttribute("uploadErrorMessage", "File is empty"); - return "fragments/settings :: file-upload-content"; - } - - if (!file.getOriginalFilename().endsWith(".json")) { - model.addAttribute("uploadErrorMessage", "Only JSON files are supported"); - return "fragments/file-upload :: file-upload-content"; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGoogleTimeline(inputStream, user); - - if ((Boolean) result.get("success")) { - model.addAttribute("uploadSuccessMessage", result.get("message")); - - // Trigger processing pipeline for imported data - try { - rawLocationPointProcessingTrigger.start(); - } catch (Exception e) { - logger.warn("Failed to trigger processing pipeline after Google Timeline import", e); - } - } else { - model.addAttribute("uploadErrorMessage", result.get("error")); - } - - return "fragments/file-upload :: file-upload-content"; - } catch (IOException e) { - model.addAttribute("uploadErrorMessage", "Error processing file: " + e.getMessage()); - return "fragments/file-upload :: file-upload-content"; - } - } - - @PostMapping("/import/geojson") - public String importGeoJson(@RequestParam("files") MultipartFile[] files, - Authentication authentication, - Model model) { - User user = (User) authentication.getPrincipal(); - - if (files.length == 0) { - model.addAttribute("uploadErrorMessage", "No files selected"); - return "fragments/settings :: file-upload-content"; - } - - int totalProcessed = 0; - int successCount = 0; - StringBuilder errorMessages = new StringBuilder(); - - for (MultipartFile file : files) { - if (file.isEmpty()) { - errorMessages.append("File ").append(file.getOriginalFilename()).append(" is empty. "); - continue; - } - - String filename = file.getOriginalFilename(); - if (filename == null || (!filename.endsWith(".geojson") && !filename.endsWith(".json"))) { - errorMessages.append("File ").append(filename).append(" is not a GeoJSON file. "); - continue; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGeoJson(inputStream, user); - - if ((Boolean) result.get("success")) { - totalProcessed += (Integer) result.get("pointsReceived"); - successCount++; - } else { - errorMessages.append("Error processing ").append(filename).append(": ") - .append(result.get("error")).append(". "); - } - } catch (IOException e) { - errorMessages.append("Error processing ").append(filename).append(": ") - .append(e.getMessage()).append(". "); - } - } - - if (successCount > 0) { - String message = "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"; - if (!errorMessages.isEmpty()) { - message += ". Errors: " + errorMessages; - } - model.addAttribute("uploadSuccessMessage", message); - - // Trigger processing pipeline for imported data - try { - rawLocationPointProcessingTrigger.start(); - } catch (Exception e) { - logger.warn("Failed to trigger processing pipeline after GeoJSON import", e); - } - } else { - model.addAttribute("uploadErrorMessage", "No files were processed successfully. " + errorMessages); - } - - return "fragments/file-upload :: file-upload-content"; - } - @GetMapping("/manage-data-content") public String getManageDataContent(Model model) { if (!dataManagementEnabled) { @@ -939,7 +732,6 @@ public class SettingsController { // Mark all raw location points as unprocessed markRawLocationPointsAsUnprocessed(currentUser); - // Trigger processing pipeline rawLocationPointProcessingTrigger.start(); model.addAttribute("successMessage", getMessage("data.clear.reprocess.success")); @@ -961,7 +753,6 @@ public class SettingsController { User currentUser = userJdbcService.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); - // Remove all data except SignificantPlaces removeAllDataExceptPlaces(currentUser); model.addAttribute("successMessage", getMessage("data.remove.all.success")); @@ -973,21 +764,16 @@ public class SettingsController { } private void clearProcessedDataExceptPlaces(User user) { - // Clear all processed data except SignificantPlaces - // Order matters due to foreign key constraints tripJdbcService.deleteAllForUser(user); processedVisitJdbcService.deleteAllForUser(user); visitJdbcService.deleteAllForUser(user); } private void markRawLocationPointsAsUnprocessed(User user) { - // Mark all raw location points for the user as unprocessed rawLocationPointJdbcService.markAllAsUnprocessedForUser(user); } private void removeAllDataExceptPlaces(User user) { - // Remove all data except SignificantPlaces - // Order matters due to foreign key constraints tripJdbcService.deleteAllForUser(user); processedVisitJdbcService.deleteAllForUser(user); visitJdbcService.deleteAllForUser(user); diff --git a/src/main/java/com/dedicatedcode/reitti/controller/api/ImportDataApiController.java b/src/main/java/com/dedicatedcode/reitti/controller/api/ImportDataApiController.java deleted file mode 100644 index bdead6ba..00000000 --- a/src/main/java/com/dedicatedcode/reitti/controller/api/ImportDataApiController.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.dedicatedcode.reitti.controller.api; - -import com.dedicatedcode.reitti.model.User; -import com.dedicatedcode.reitti.service.ImportHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.PostMapping; -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.multipart.MultipartFile; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1") -public class ImportDataApiController { - - private static final Logger logger = LoggerFactory.getLogger(ImportDataApiController.class); - - private final ImportHandler importHandler; - - @Autowired - public ImportDataApiController(ImportHandler importHandler) { - this.importHandler = importHandler; - } - - @PostMapping("/import/gpx") - public ResponseEntity importGpx( - @RequestParam("files") MultipartFile[] files) { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - User user = (User) authentication.getPrincipal(); - - if (files.length == 0) { - return ResponseEntity.badRequest().body(Map.of("success", false, "error", "No files provided")); - } - - int totalProcessed = 0; - int successCount = 0; - List errors = new ArrayList<>(); - - for (MultipartFile file : files) { - if (file.isEmpty()) { - errors.add("File " + file.getOriginalFilename() + " is empty"); - continue; - } - - if (!file.getOriginalFilename().endsWith(".gpx")) { - errors.add("File " + file.getOriginalFilename() + " is not a GPX file"); - continue; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGpx(inputStream, user); - - if ((Boolean) result.get("success")) { - totalProcessed += (Integer) result.get("pointsReceived"); - successCount++; - } else { - errors.add("Error processing " + file.getOriginalFilename() + ": " + result.get("error")); - } - } catch (IOException e) { - logger.error("Error processing GPX file: " + file.getOriginalFilename(), e); - errors.add("Error processing " + file.getOriginalFilename() + ": " + e.getMessage()); - } - } - - Map response = new HashMap<>(); - if (successCount > 0) { - response.put("success", true); - response.put("message", "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"); - response.put("pointsReceived", totalProcessed); - response.put("filesProcessed", successCount); - if (!errors.isEmpty()) { - response.put("errors", errors); - } - return ResponseEntity.accepted().body(response); - } else { - response.put("success", false); - response.put("error", "No files were processed successfully"); - response.put("errors", errors); - return ResponseEntity.badRequest().body(response); - } - } - - @PostMapping("/import/geojson") - public ResponseEntity importGeoJson( - @RequestParam("files") MultipartFile[] files) { - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - User user = (User) authentication.getPrincipal(); - - if (files.length == 0) { - return ResponseEntity.badRequest().body(Map.of("success", false, "error", "No files provided")); - } - - int totalProcessed = 0; - int successCount = 0; - List errors = new ArrayList<>(); - - for (MultipartFile file : files) { - if (file.isEmpty()) { - errors.add("File " + file.getOriginalFilename() + " is empty"); - continue; - } - - String filename = file.getOriginalFilename(); - if (filename == null || (!filename.endsWith(".geojson") && !filename.endsWith(".json"))) { - errors.add("File " + filename + " is not a GeoJSON file"); - continue; - } - - try (InputStream inputStream = file.getInputStream()) { - Map result = importHandler.importGeoJson(inputStream, user); - - if ((Boolean) result.get("success")) { - totalProcessed += (Integer) result.get("pointsReceived"); - successCount++; - } else { - errors.add("Error processing " + filename + ": " + result.get("error")); - } - } catch (IOException e) { - logger.error("Error processing GeoJSON file: " + filename, e); - errors.add("Error processing " + filename + ": " + e.getMessage()); - } - } - - Map response = new HashMap<>(); - if (successCount > 0) { - response.put("success", true); - response.put("message", "Successfully processed " + successCount + " file(s) with " + totalProcessed + " location points"); - response.put("pointsReceived", totalProcessed); - response.put("filesProcessed", successCount); - if (!errors.isEmpty()) { - response.put("errors", errors); - } - return ResponseEntity.accepted().body(response); - } else { - response.put("success", false); - response.put("error", "No files were processed successfully"); - response.put("errors", errors); - return ResponseEntity.badRequest().body(response); - } - } - -} diff --git a/src/main/java/com/dedicatedcode/reitti/dto/LocationDataRequest.java b/src/main/java/com/dedicatedcode/reitti/dto/LocationDataRequest.java index c5c6a554..3525404b 100644 --- a/src/main/java/com/dedicatedcode/reitti/dto/LocationDataRequest.java +++ b/src/main/java/com/dedicatedcode/reitti/dto/LocationDataRequest.java @@ -93,5 +93,16 @@ public class LocationDataRequest { public void setActivity(String activity) { this.activity = activity; } + + @Override + public String toString() { + return "LocationPoint{" + + "latitude=" + latitude + + ", longitude=" + longitude + + ", timestamp='" + timestamp + '\'' + + ", accuracyMeters=" + accuracyMeters + + ", activity='" + activity + '\'' + + '}'; + } } } diff --git a/src/main/java/com/dedicatedcode/reitti/event/LocationProcessEvent.java b/src/main/java/com/dedicatedcode/reitti/event/LocationProcessEvent.java index 67c1004e..8262f071 100644 --- a/src/main/java/com/dedicatedcode/reitti/event/LocationProcessEvent.java +++ b/src/main/java/com/dedicatedcode/reitti/event/LocationProcessEvent.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; import java.time.Instant; import java.util.List; +import java.util.Objects; public class LocationProcessEvent implements Serializable { private final String username; @@ -35,4 +36,16 @@ public class LocationProcessEvent implements Serializable { public Instant getLatest() { return latest; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + LocationProcessEvent that = (LocationProcessEvent) o; + return Objects.equals(username, that.username) && Objects.equals(earliest, that.earliest) && Objects.equals(latest, that.latest); + } + + @Override + public int hashCode() { + return Objects.hash(username, earliest, latest); + } } diff --git a/src/main/java/com/dedicatedcode/reitti/repository/ProcessedVisitJdbcService.java b/src/main/java/com/dedicatedcode/reitti/repository/ProcessedVisitJdbcService.java index a5d842ff..36271f8c 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/ProcessedVisitJdbcService.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/ProcessedVisitJdbcService.java @@ -63,7 +63,7 @@ public class ProcessedVisitJdbcService { public List findByUserAndTimeOverlap(User user, Instant startTime, Instant endTime) { String sql = "SELECT pv.* " + "FROM processed_visits pv " + - "WHERE pv.user_id = ? AND pv.start_time <= ? AND pv.end_time >= ?"; + "WHERE pv.user_id = ? AND pv.start_time <= ? AND pv.end_time >= ? ORDER BY pv.start_time"; return jdbcTemplate.query(sql, PROCESSED_VISIT_ROW_MAPPER, user.getId(), Timestamp.from(endTime), Timestamp.from(startTime)); } @@ -71,7 +71,7 @@ public class ProcessedVisitJdbcService { public Optional findByUserAndId(User user, long id) { String sql = "SELECT pv.* " + "FROM processed_visits pv " + - "WHERE pv.user_id = ? AND pv.id = ?"; + "WHERE pv.user_id = ? AND pv.id = ? ORDER BY pv.start_time"; List results = jdbcTemplate.query(sql, PROCESSED_VISIT_ROW_MAPPER, user.getId(), id); return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); } diff --git a/src/main/java/com/dedicatedcode/reitti/service/ImportHandler.java b/src/main/java/com/dedicatedcode/reitti/service/ImportHandler.java deleted file mode 100644 index 2c928b27..00000000 --- a/src/main/java/com/dedicatedcode/reitti/service/ImportHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.dedicatedcode.reitti.service; - -import com.dedicatedcode.reitti.model.User; -import com.dedicatedcode.reitti.service.importer.GeoJsonImporter; -import com.dedicatedcode.reitti.service.importer.GoogleRecordsImporter; -import com.dedicatedcode.reitti.service.importer.GoogleTimelineImporter; -import com.dedicatedcode.reitti.service.importer.GpxImporter; -import org.springframework.stereotype.Service; - -import java.io.InputStream; -import java.util.Map; - -@Service -public class ImportHandler { - - private final GoogleRecordsImporter googleRecordsImporter; - private final GoogleTimelineImporter googleTimelineImporter; - private final GpxImporter gpxImporter; - private final GeoJsonImporter geoJsonImporter; - - public ImportHandler( - GoogleRecordsImporter googleRecordsImporter, - GoogleTimelineImporter googleTimelineImporter, - GpxImporter gpxImporter, - GeoJsonImporter geoJsonImporter) { - this.googleRecordsImporter = googleRecordsImporter; - this.googleTimelineImporter = googleTimelineImporter; - this.gpxImporter = gpxImporter; - this.geoJsonImporter = geoJsonImporter; - } - - - public Map importGoogleRecords(InputStream inputStream, User user) { - return googleRecordsImporter.importGoogleRecords(inputStream, user); - } - - public Map importGoogleTimeline(InputStream inputStream, User user) { - return googleTimelineImporter.importGoogleTimeline(inputStream, user); - } - - public Map importGpx(InputStream inputStream, User user) { - return gpxImporter.importGpx(inputStream, user); - } - - public Map importGeoJson(InputStream inputStream, User user) { - return geoJsonImporter.importGeoJson(inputStream, user); - } -} diff --git a/src/main/java/com/dedicatedcode/reitti/service/ImportStateHolder.java b/src/main/java/com/dedicatedcode/reitti/service/ImportStateHolder.java new file mode 100644 index 00000000..833dd1b8 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/ImportStateHolder.java @@ -0,0 +1,22 @@ +package com.dedicatedcode.reitti.service; + +import org.springframework.stereotype.Service; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Service +public class ImportStateHolder { + private final AtomicBoolean importRunning = new AtomicBoolean(false); + + public void importStarted() { + importRunning.set(true); + } + + public boolean isImportRunning() { + return importRunning.get(); + } + + public void importFinished() { + importRunning.set(false); + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManager.java b/src/main/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManager.java index 130fea4f..d3fb711b 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManager.java +++ b/src/main/java/com/dedicatedcode/reitti/service/geocoding/DefaultGeocodeServiceManager.java @@ -138,7 +138,6 @@ public class DefaultGeocodeServiceManager implements GeocodeServiceManager { district = address.path("city_district").asText(""); } - if (label.isEmpty() && !street.isEmpty()) { label = street; } diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporter.java new file mode 100644 index 00000000..82bac1dd --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporter.java @@ -0,0 +1,127 @@ +package com.dedicatedcode.reitti.service.importer; + +import com.dedicatedcode.reitti.dto.LocationDataRequest; +import com.dedicatedcode.reitti.model.User; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +public abstract class BaseGoogleTimelineImporter { + + private static final Logger logger = LoggerFactory.getLogger(BaseGoogleTimelineImporter.class); + + protected final ObjectMapper objectMapper; + protected final ImportBatchProcessor batchProcessor; + protected final int minStayPointDetectionPoints; + protected final int distanceThresholdMeters; + protected final int mergeThresholdSeconds; + + public BaseGoogleTimelineImporter(ObjectMapper objectMapper, + ImportBatchProcessor batchProcessor, + @Value("${reitti.staypoint.min-points}") int minStayPointDetectionPoints, + @Value("${reitti.staypoint.distance-threshold-meters}") int distanceThresholdMeters, + @Value("${reitti.visit.merge-threshold-seconds}") int mergeThresholdSeconds) { + this.objectMapper = objectMapper; + this.batchProcessor = batchProcessor; + this.minStayPointDetectionPoints = minStayPointDetectionPoints; + this.distanceThresholdMeters = distanceThresholdMeters; + this.mergeThresholdSeconds = mergeThresholdSeconds; + } + + protected int handleVisit(User user, ZonedDateTime startTime, ZonedDateTime endTime, LatLng latLng, List batch) { + logger.info("Found visit at [{}] from start [{}] to end [{}]. Will insert at least [{}] synthetic geo locations.", latLng, startTime, endTime, minStayPointDetectionPoints); + createAndScheduleLocationPoint(latLng, startTime, user, batch); + int count = 1; + long durationBetween = Duration.between(startTime.toInstant(), endTime.toInstant()).toSeconds(); + if (durationBetween > mergeThresholdSeconds) { + long increment = 60; + ZonedDateTime currentTime = startTime.plusSeconds(increment); + while (currentTime.isBefore(endTime)) { + createAndScheduleLocationPoint(latLng, currentTime, user, batch); + count+=1; + currentTime = currentTime.plusSeconds(increment); + } + logger.debug("Inserting synthetic points into import to simulate stays at [{}] from [{}] till [{}]", latLng, startTime, endTime); + } else { + logger.info("Skipping creating synthetic points at [{}] since duration was less then [{}] seconds ", latLng, mergeThresholdSeconds); + } + createAndScheduleLocationPoint(latLng, endTime, user, batch); + return count + 1; + } + + protected void createAndScheduleLocationPoint(LatLng latLng, ZonedDateTime timestamp, User user, List batch) { + LocationDataRequest.LocationPoint point = new LocationDataRequest.LocationPoint(); + point.setLatitude(latLng.latitude); + point.setLongitude(latLng.longitude); + point.setTimestamp(timestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + point.setAccuracyMeters(10.0); + batch.add(point); + logger.trace("Created location point at [{}]", point); + if (batch.size() >= batchProcessor.getBatchSize()) { + batchProcessor.sendToQueue(user, batch); + batch.clear(); + } + } + + protected Optional parseLatLng(String input) { + try { + String[] coords = parseLatLngString(input); + if (coords == null) { + return Optional.empty(); + } + return Optional.of(new LatLng(Double.parseDouble(coords[0]), Double.parseDouble(coords[1]))); + } catch (NumberFormatException e) { + logger.warn("Error parsing LatLng string: {}", input); + return Optional.empty(); + } + } + + protected record LatLng(double latitude, double longitude) {} + + /** + * Parses a LatLng string in format "53.8633043°, 10.7011529°" or "geo:55.605843,13.007508" to extract latitude and longitude + */ + protected String[] parseLatLngString(String latLngStr) { + if (latLngStr == null || latLngStr.trim().isEmpty()) { + return null; + } + + try { + String cleaned = latLngStr.trim(); + + // Handle geo: format + if (cleaned.startsWith("geo:")) { + cleaned = cleaned.substring(4); // Remove "geo:" prefix + } else { + // Handle degree format - remove degree symbols + cleaned = cleaned.replace("°", ""); + } + + String[] parts = cleaned.split(","); + + if (parts.length != 2) { + return null; + } + + String latStr = parts[0].trim(); + String lngStr = parts[1].trim(); + + // Validate that they are valid numbers + Double.parseDouble(latStr); + Double.parseDouble(lngStr); + + return new String[]{latStr, lngStr}; + } catch (Exception e) { + logger.warn("Failed to parse LatLng string: {}", latLngStr); + return null; + } + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GeoJsonImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GeoJsonImporter.java index adbe51e0..5ce3d1a5 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/importer/GeoJsonImporter.java +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/GeoJsonImporter.java @@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service.importer; import com.dedicatedcode.reitti.dto.LocationDataRequest; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -21,10 +22,12 @@ public class GeoJsonImporter { private static final Logger logger = LoggerFactory.getLogger(GeoJsonImporter.class); private final ObjectMapper objectMapper; + private final ImportStateHolder stateHolder; private final ImportBatchProcessor batchProcessor; - public GeoJsonImporter(ObjectMapper objectMapper, ImportBatchProcessor batchProcessor) { + public GeoJsonImporter(ObjectMapper objectMapper, ImportStateHolder stateHolder, ImportBatchProcessor batchProcessor) { this.objectMapper = objectMapper; + this.stateHolder = stateHolder; this.batchProcessor = batchProcessor; } @@ -32,6 +35,7 @@ public class GeoJsonImporter { AtomicInteger processedCount = new AtomicInteger(0); try { + stateHolder.importStarted(); JsonNode rootNode = objectMapper.readTree(inputStream); // Check if it's a valid GeoJSON @@ -101,6 +105,8 @@ public class GeoJsonImporter { } catch (IOException e) { logger.error("Error processing GeoJSON file", e); return Map.of("success", false, "error", "Error processing GeoJSON file: " + e.getMessage()); + } finally { + stateHolder.importFinished(); } } diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporter.java new file mode 100644 index 00000000..406e7eac --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporter.java @@ -0,0 +1,101 @@ +package com.dedicatedcode.reitti.service.importer; + +import com.dedicatedcode.reitti.dto.LocationDataRequest; +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; +import com.dedicatedcode.reitti.service.importer.dto.GoogleTimelineData; +import com.dedicatedcode.reitti.service.importer.dto.SemanticSegment; +import com.dedicatedcode.reitti.service.importer.dto.TimelinePathPoint; +import com.dedicatedcode.reitti.service.importer.dto.Visit; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class GoogleAndroidTimelineImporter extends BaseGoogleTimelineImporter { + + private static final Logger logger = LoggerFactory.getLogger(GoogleAndroidTimelineImporter.class); + private final ImportStateHolder stateHolder; + + public GoogleAndroidTimelineImporter(ObjectMapper objectMapper, + ImportStateHolder stateHolder, + ImportBatchProcessor batchProcessor, + @Value("${reitti.staypoint.min-points}") int minStayPointDetectionPoints, + @Value("${reitti.staypoint.distance-threshold-meters}") int distanceThresholdMeters, + @Value("${reitti.visit.merge-threshold-seconds}") int mergeThresholdSeconds) { + super(objectMapper, batchProcessor, minStayPointDetectionPoints, distanceThresholdMeters, mergeThresholdSeconds); + this.stateHolder = stateHolder; + } + + public Map importTimeline(InputStream inputStream, User user) { + AtomicInteger processedCount = new AtomicInteger(0); + + try { + this.stateHolder.importStarted(); + JsonFactory factory = objectMapper.getFactory(); + JsonParser parser = factory.createParser(inputStream); + + List batch = new ArrayList<>(batchProcessor.getBatchSize()); + + GoogleTimelineData timelineData = objectMapper.readValue(parser, GoogleTimelineData.class); + List semanticSegments = timelineData.getSemanticSegments(); + logger.info("Found {} semantic segments", semanticSegments.size()); + for (SemanticSegment semanticSegment : semanticSegments) { + ZonedDateTime start = ZonedDateTime.parse(semanticSegment.getStartTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME).withNano(0); + ZonedDateTime end = ZonedDateTime.parse(semanticSegment.getEndTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME).withNano(0); + if (semanticSegment.getVisit() != null) { + Visit visit = semanticSegment.getVisit(); + Optional latLng = parseLatLng(visit.getTopCandidate().getPlaceLocation().getLatLng()); + if (latLng.isPresent()) { + latLng.ifPresent(lng -> processedCount.addAndGet(handleVisit(user, start, end, lng, batch))); + } + } + + if (semanticSegment.getTimelinePath() != null) { + List timelinePath = semanticSegment.getTimelinePath(); + logger.info("Found timeline path from start [{}] to end [{}]. Will insert [{}] synthetic geo locations based on timeline path.", semanticSegment.getStartTime(), semanticSegment.getEndTime(), timelinePath.size()); + for (TimelinePathPoint timelinePathPoint : timelinePath) { + parseLatLng(timelinePathPoint.getPoint()).ifPresent(location -> { + createAndScheduleLocationPoint(location, ZonedDateTime.parse(timelinePathPoint.getTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME).withNano(0), user, batch); + processedCount.incrementAndGet(); + }); + } + } + } + + // Process any remaining locations + if (!batch.isEmpty()) { + batchProcessor.sendToQueue(user, batch); + } + + logger.info("Successfully imported and queued {} location points from Google Timeline for user {}", + processedCount.get(), user.getUsername()); + + return Map.of( + "success", true, + "message", "Successfully queued " + processedCount.get() + " location points for processing", + "pointsReceived", processedCount.get() + ); + + } catch (IOException e) { + logger.error("Error processing Google Timeline file", e); + return Map.of("success", false, "error", "Error processing Google Timeline file: " + e.getMessage()); + } finally { + stateHolder.importFinished(); + } + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporter.java new file mode 100644 index 00000000..c57158a6 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporter.java @@ -0,0 +1,97 @@ +package com.dedicatedcode.reitti.service.importer; + +import com.dedicatedcode.reitti.dto.LocationDataRequest; +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; +import com.dedicatedcode.reitti.service.importer.dto.ios.IOSSemanticSegment; +import com.dedicatedcode.reitti.service.importer.dto.ios.IOSVisit; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class GoogleIOSTimelineImporter extends BaseGoogleTimelineImporter { + private static final Logger logger = LoggerFactory.getLogger(GoogleIOSTimelineImporter.class); + private final ImportStateHolder stateHolder; + + public GoogleIOSTimelineImporter(ObjectMapper objectMapper, + ImportStateHolder stateHolder, + ImportBatchProcessor batchProcessor, + @Value("${reitti.staypoint.min-points}") int minStayPointDetectionPoints, + @Value("${reitti.staypoint.distance-threshold-meters}") int distanceThresholdMeters, + @Value("${reitti.visit.merge-threshold-seconds}") int mergeThresholdSeconds) { + super(objectMapper, batchProcessor, minStayPointDetectionPoints, distanceThresholdMeters, mergeThresholdSeconds); + this.stateHolder = stateHolder; + } + + public Map importTimeline(InputStream inputStream, User user) { + AtomicInteger processedCount = new AtomicInteger(0); + + try { + stateHolder.importStarted(); + JsonFactory factory = objectMapper.getFactory(); + JsonParser parser = factory.createParser(inputStream); + + List batch = new ArrayList<>(batchProcessor.getBatchSize()); + + List semanticSegments = objectMapper.readValue(parser, new TypeReference<>() {}); + logger.info("Found {} semantic segments", semanticSegments.size()); + for (IOSSemanticSegment semanticSegment : semanticSegments) { + ZonedDateTime start = ZonedDateTime.parse(semanticSegment.getStartTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + ZonedDateTime end = ZonedDateTime.parse(semanticSegment.getEndTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + if (semanticSegment.getVisit() != null) { + IOSVisit visit = semanticSegment.getVisit(); + Optional latLng = parseLatLng(visit.getTopCandidate().getPlaceLocation()); + latLng.ifPresent(lng -> processedCount.addAndGet(handleVisit(user, start, end, lng, batch))); + } + + if (semanticSegment.getTimelinePath() != null) { + List timelinePath = semanticSegment.getTimelinePath(); + logger.info("Found timeline path from start [{}] to end [{}]. Will insert [{}] synthetic geo locations based on timeline path.", semanticSegment.getStartTime(), semanticSegment.getEndTime(), timelinePath.size()); + for (com.dedicatedcode.reitti.service.importer.dto.ios.TimelinePathPoint timelinePathPoint : timelinePath) { + parseLatLng(timelinePathPoint.getPoint()).ifPresent(location -> { + ZonedDateTime current = start.plusMinutes(Long.parseLong(timelinePathPoint.getDurationMinutesOffsetFromStartTime())); + createAndScheduleLocationPoint(location, current, user, batch); + processedCount.incrementAndGet(); + }); + } + } + } + + // Process any remaining locations + if (!batch.isEmpty()) { + batchProcessor.sendToQueue(user, batch); + } + + logger.info("Successfully imported and queued {} location points from Google Timeline for user {}", + processedCount.get(), user.getUsername()); + + return Map.of( + "success", true, + "message", "Successfully queued " + processedCount.get() + " location points for processing", + "pointsReceived", processedCount.get() + ); + + } catch (IOException e) { + logger.error("Error processing Google Timeline file", e); + return Map.of("success", false, "error", "Error processing Google Timeline file: " + e.getMessage()); + } finally { + stateHolder.importFinished(); + } + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporter.java index f4e8eda5..79a9ab26 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporter.java +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporter.java @@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service.importer; import com.dedicatedcode.reitti.dto.LocationDataRequest; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; @@ -24,10 +25,12 @@ public class GoogleRecordsImporter { private static final Logger logger = LoggerFactory.getLogger(GoogleRecordsImporter.class); private final ObjectMapper objectMapper; + private final ImportStateHolder stateHolder; private final ImportBatchProcessor batchProcessor; - public GoogleRecordsImporter(ObjectMapper objectMapper, ImportBatchProcessor batchProcessor) { + public GoogleRecordsImporter(ObjectMapper objectMapper, ImportStateHolder stateHolder, ImportBatchProcessor batchProcessor) { this.objectMapper = objectMapper; + this.stateHolder = stateHolder; this.batchProcessor = batchProcessor; } @@ -35,6 +38,7 @@ public class GoogleRecordsImporter { AtomicInteger processedCount = new AtomicInteger(0); try { + stateHolder.importStarted(); // Use Jackson's streaming API to process the file JsonFactory factory = objectMapper.getFactory(); JsonParser parser = factory.createParser(inputStream); @@ -76,6 +80,8 @@ public class GoogleRecordsImporter { } catch (IOException e) { logger.error("Error processing Google Records file", e); return Map.of("success", false, "error", "Error processing Google Records file: " + e.getMessage()); + } finally { + stateHolder.importFinished(); } } diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporter.java deleted file mode 100644 index c86d53b4..00000000 --- a/src/main/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporter.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.dedicatedcode.reitti.service.importer; - -import com.dedicatedcode.reitti.dto.LocationDataRequest; -import com.dedicatedcode.reitti.model.User; -import com.dedicatedcode.reitti.service.importer.dto.GoogleTimelineData; -import com.dedicatedcode.reitti.service.importer.dto.SemanticSegment; -import com.dedicatedcode.reitti.service.importer.dto.TimelinePathPoint; -import com.dedicatedcode.reitti.service.importer.dto.Visit; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.io.InputStream; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; - -@Component -public class GoogleTimelineImporter { - - private static final Logger logger = LoggerFactory.getLogger(GoogleTimelineImporter.class); - private static final Random random = new Random(); - - private final ObjectMapper objectMapper; - private final ImportBatchProcessor batchProcessor; - private final int minStayPointDetectionPoints; - private final int distanceThresholdMeters; - - public GoogleTimelineImporter(ObjectMapper objectMapper, - ImportBatchProcessor batchProcessor, - @Value("${reitti.staypoint.min-points}") int minStayPointDetectionPoints, - @Value("${reitti.staypoint.distance-threshold-meters}") int distanceThresholdMeters - ) { - this.objectMapper = objectMapper; - this.batchProcessor = batchProcessor; - this.minStayPointDetectionPoints = minStayPointDetectionPoints; - this.distanceThresholdMeters = distanceThresholdMeters; - } - - public Map importGoogleTimeline(InputStream inputStream, User user) { - AtomicInteger processedCount = new AtomicInteger(0); - - try { - // Use Jackson's streaming API to process the file - JsonFactory factory = objectMapper.getFactory(); - JsonParser parser = factory.createParser(inputStream); - - List batch = new ArrayList<>(batchProcessor.getBatchSize()); - boolean foundData = false; - - GoogleTimelineData timelineData = objectMapper.readValue(parser, GoogleTimelineData.class); - List semanticSegments = timelineData.getSemanticSegments(); - logger.info("Found {} semantic segments", semanticSegments.size()); - for (SemanticSegment semanticSegment : semanticSegments) { - if (semanticSegment.getVisit() != null) { - Visit visit = semanticSegment.getVisit(); - logger.info("Found visit at [{}] from start [{}] to end [{}]. Will insert at least [{}] synthetic geo locations.", visit.getTopCandidate().getPlaceLocation().getLatLng(), semanticSegment.getStartTime(), semanticSegment.getEndTime(), minStayPointDetectionPoints); - - Optional latLng = parseLatLng(visit.getTopCandidate().getPlaceLocation().getLatLng()); - if (latLng.isPresent()) { - createAndScheduleLocationPoint(latLng.get(), semanticSegment.getStartTime(), user, batch); - processedCount.incrementAndGet(); - ZonedDateTime startTime = ZonedDateTime.parse(semanticSegment.getStartTime()); - ZonedDateTime endTime = ZonedDateTime.parse(semanticSegment.getEndTime()); - long durationBetween = Duration.between(startTime.toInstant(), endTime.toInstant()).toSeconds(); - long increment = Math.max(10, durationBetween / (minStayPointDetectionPoints * 10L)); - ZonedDateTime currentTime = startTime.plusSeconds(increment); - while (currentTime.isBefore(endTime)) { - // Move randomly around the visit location within the distance threshold - LatLng randomizedLocation = addRandomOffset(latLng.get(), distanceThresholdMeters / 3); - createAndScheduleLocationPoint(randomizedLocation, currentTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), user, batch); - processedCount.incrementAndGet(); - currentTime = currentTime.plusSeconds(increment); - } - createAndScheduleLocationPoint(latLng.get(), semanticSegment.getEndTime(), user, batch); - processedCount.incrementAndGet(); - - } - } - - if (semanticSegment.getTimelinePath() != null) { - List timelinePath = semanticSegment.getTimelinePath(); - logger.info("Found timeline path from start [{}] to end [{}]. Will insert [{}] synthetic geo locations based on timeline path.", semanticSegment.getStartTime(), semanticSegment.getEndTime(), timelinePath.size()); - for (TimelinePathPoint timelinePathPoint : timelinePath) { - parseLatLng(timelinePathPoint.getPoint()).ifPresent(location -> { - createAndScheduleLocationPoint(location, timelinePathPoint.getTime(), user, batch); - processedCount.incrementAndGet(); - }); - } - } - } - - // Process any remaining locations - if (!batch.isEmpty()) { - batchProcessor.sendToQueue(user, batch); - } - - logger.info("Successfully imported and queued {} location points from Google Timeline for user {}", - processedCount.get(), user.getUsername()); - - return Map.of( - "success", true, - "message", "Successfully queued " + processedCount.get() + " location points for processing", - "pointsReceived", processedCount.get() - ); - - } catch (IOException e) { - logger.error("Error processing Google Timeline file", e); - return Map.of("success", false, "error", "Error processing Google Timeline file: " + e.getMessage()); - } - } - - - void createAndScheduleLocationPoint(LatLng latLng, String timestamp, User user, List batch) { - LocationDataRequest.LocationPoint point = new LocationDataRequest.LocationPoint(); - point.setLatitude(latLng.latitude); - point.setLongitude(latLng.longitude); - point.setTimestamp(timestamp); - point.setAccuracyMeters(10.0); - batch.add(point); - if (batch.size() >= batchProcessor.getBatchSize()) { - batchProcessor.sendToQueue(user, batch); - batch.clear(); - } - } - - private Optional parseLatLng(String input) { - try { - String[] coords = parseLatLngString(input); - if (coords == null) { - return Optional.empty(); - } - return Optional.of(new LatLng(Double.parseDouble(coords[0]), Double.parseDouble(coords[1]))); - } catch (NumberFormatException e) { - logger.warn("Error parsing LatLng string: {}", input); - return Optional.empty(); - } - } - /** - * Adds a random offset to a location within the specified distance threshold - */ - private LatLng addRandomOffset(LatLng original, int maxDistanceMeters) { - // Convert distance to approximate degrees (rough approximation) - // 1 degree latitude ≈ 111,000 meters - // 1 degree longitude ≈ 111,000 * cos(latitude) meters - double latOffsetDegrees = (maxDistanceMeters / 111000.0) * (random.nextDouble() * 2 - 1); - double lonOffsetDegrees = (maxDistanceMeters / (111000.0 * Math.cos(Math.toRadians(original.latitude)))) * (random.nextDouble() * 2 - 1); - - // Ensure we don't exceed the maximum distance by scaling if necessary - double actualDistance = Math.sqrt(latOffsetDegrees * latOffsetDegrees + lonOffsetDegrees * lonOffsetDegrees) * 111000.0; - if (actualDistance > maxDistanceMeters) { - double scale = maxDistanceMeters / actualDistance; - latOffsetDegrees *= scale; - lonOffsetDegrees *= scale; - } - - return new LatLng( - original.latitude + latOffsetDegrees, - original.longitude + lonOffsetDegrees - ); - } - - private record LatLng(double latitude, double longitude) {} - - /** - * Parses a LatLng string in format "53.8633043°, 10.7011529°" or "geo:55.605843,13.007508" to extract latitude and longitude - */ - private String[] parseLatLngString(String latLngStr) { - if (latLngStr == null || latLngStr.trim().isEmpty()) { - return null; - } - - try { - String cleaned = latLngStr.trim(); - - // Handle geo: format - if (cleaned.startsWith("geo:")) { - cleaned = cleaned.substring(4); // Remove "geo:" prefix - } else { - // Handle degree format - remove degree symbols - cleaned = cleaned.replace("°", ""); - } - - String[] parts = cleaned.split(","); - - if (parts.length != 2) { - return null; - } - - String latStr = parts[0].trim(); - String lngStr = parts[1].trim(); - - // Validate that they are valid numbers - Double.parseDouble(latStr); - Double.parseDouble(lngStr); - - return new String[]{latStr, lngStr}; - } catch (Exception e) { - logger.warn("Failed to parse LatLng string: {}", latLngStr); - return null; - } - } -} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/GpxImporter.java b/src/main/java/com/dedicatedcode/reitti/service/importer/GpxImporter.java index 0c063634..84176d23 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/importer/GpxImporter.java +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/GpxImporter.java @@ -2,6 +2,7 @@ package com.dedicatedcode.reitti.service.importer; import com.dedicatedcode.reitti.dto.LocationDataRequest; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -22,10 +23,12 @@ import java.util.concurrent.atomic.AtomicInteger; public class GpxImporter { private static final Logger logger = LoggerFactory.getLogger(GpxImporter.class); - + + private final ImportStateHolder stateHolder; private final ImportBatchProcessor batchProcessor; - public GpxImporter(ImportBatchProcessor batchProcessor) { + public GpxImporter(ImportStateHolder stateHolder, ImportBatchProcessor batchProcessor) { + this.stateHolder = stateHolder; this.batchProcessor = batchProcessor; } @@ -33,6 +36,7 @@ public class GpxImporter { AtomicInteger processedCount = new AtomicInteger(0); try { + stateHolder.importStarted(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(inputStream); @@ -84,6 +88,8 @@ public class GpxImporter { } catch (Exception e) { logger.error("Error processing GPX file", e); return Map.of("success", false, "error", "Error processing GPX file: " + e.getMessage()); + } finally { + stateHolder.importFinished(); } } diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/ImportBatchProcessor.java b/src/main/java/com/dedicatedcode/reitti/service/importer/ImportBatchProcessor.java index 31d548e7..e51dd8fd 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/importer/ImportBatchProcessor.java +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/ImportBatchProcessor.java @@ -10,6 +10,7 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.List; @Component @@ -30,7 +31,7 @@ public class ImportBatchProcessor { public void sendToQueue(User user, List batch) { LocationDataEvent event = new LocationDataEvent( user.getUsername(), - batch + new ArrayList<>(batch) ); rabbitTemplate.convertAndSend( RabbitMQConfig.EXCHANGE_NAME, diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSActivity.java b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSActivity.java new file mode 100644 index 00000000..2a61e32d --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSActivity.java @@ -0,0 +1,41 @@ +package com.dedicatedcode.reitti.service.importer.dto.ios; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({"topCandidate"}) +public class IOSActivity { + + @JsonProperty("start") + private String start; + + @JsonProperty("end") + private String end; + + @JsonProperty("distanceMeters") + private Double distanceMeters; + + public String getStart() { + return start; + } + + public void setStart(String start) { + this.start = start; + } + + public String getEnd() { + return end; + } + + public void setEnd(String end) { + this.end = end; + } + + public Double getDistanceMeters() { + return distanceMeters; + } + + public void setDistanceMeters(Double distanceMeters) { + this.distanceMeters = distanceMeters; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSSemanticSegment.java b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSSemanticSegment.java new file mode 100644 index 00000000..d6e367b9 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSSemanticSegment.java @@ -0,0 +1,87 @@ +package com.dedicatedcode.reitti.service.importer.dto.ios; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties({"timelineMemory"}) +public class IOSSemanticSegment { + + @JsonProperty("startTime") + private String startTime; + + @JsonProperty("endTime") + private String endTime; + + @JsonProperty("startTimeTimezoneUtcOffsetMinutes") + private Integer startTimeTimezoneUtcOffsetMinutes; + + @JsonProperty("endTimeTimezoneUtcOffsetMinutes") + private Integer endTimeTimezoneUtcOffsetMinutes; + + @JsonProperty("timelinePath") + private List timelinePath; + + @JsonProperty("visit") + private IOSVisit visit; + + @JsonProperty("activity") + private IOSActivity activity; + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public Integer getStartTimeTimezoneUtcOffsetMinutes() { + return startTimeTimezoneUtcOffsetMinutes; + } + + public void setStartTimeTimezoneUtcOffsetMinutes(Integer startTimeTimezoneUtcOffsetMinutes) { + this.startTimeTimezoneUtcOffsetMinutes = startTimeTimezoneUtcOffsetMinutes; + } + + public Integer getEndTimeTimezoneUtcOffsetMinutes() { + return endTimeTimezoneUtcOffsetMinutes; + } + + public void setEndTimeTimezoneUtcOffsetMinutes(Integer endTimeTimezoneUtcOffsetMinutes) { + this.endTimeTimezoneUtcOffsetMinutes = endTimeTimezoneUtcOffsetMinutes; + } + + public List getTimelinePath() { + return timelinePath; + } + + public void setTimelinePath(List timelinePath) { + this.timelinePath = timelinePath; + } + + public IOSVisit getVisit() { + return visit; + } + + public void setVisit(IOSVisit visit) { + this.visit = visit; + } + + public IOSActivity getActivity() { + return activity; + } + + public void setActivity(IOSActivity activity) { + this.activity = activity; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSTopCandidate.java b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSTopCandidate.java new file mode 100644 index 00000000..8fd1f54f --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSTopCandidate.java @@ -0,0 +1,61 @@ +package com.dedicatedcode.reitti.service.importer.dto.ios; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IOSTopCandidate { + + @JsonProperty("placeID") + private String placeId; + + @JsonProperty("semanticType") + private String semanticType; + + @JsonProperty("probability") + private Double probability; + + @JsonProperty("placeLocation") + private String placeLocation; + + @JsonProperty("type") + private String type; + + public String getPlaceId() { + return placeId; + } + + public void setPlaceId(String placeId) { + this.placeId = placeId; + } + + public String getSemanticType() { + return semanticType; + } + + public void setSemanticType(String semanticType) { + this.semanticType = semanticType; + } + + public Double getProbability() { + return probability; + } + + public void setProbability(Double probability) { + this.probability = probability; + } + + public String getPlaceLocation() { + return placeLocation; + } + + public void setPlaceLocation(String placeLocation) { + this.placeLocation = placeLocation; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSVisit.java b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSVisit.java new file mode 100644 index 00000000..23561cb1 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/IOSVisit.java @@ -0,0 +1,39 @@ +package com.dedicatedcode.reitti.service.importer.dto.ios; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IOSVisit { + + @JsonProperty("hierarchyLevel") + private Integer hierarchyLevel; + + @JsonProperty("probability") + private Double probability; + + @JsonProperty("topCandidate") + private IOSTopCandidate topCandidate; + + public Integer getHierarchyLevel() { + return hierarchyLevel; + } + + public void setHierarchyLevel(Integer hierarchyLevel) { + this.hierarchyLevel = hierarchyLevel; + } + + public Double getProbability() { + return probability; + } + + public void setProbability(Double probability) { + this.probability = probability; + } + + public IOSTopCandidate getTopCandidate() { + return topCandidate; + } + + public void setTopCandidate(IOSTopCandidate topCandidate) { + this.topCandidate = topCandidate; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/TimelinePathPoint.java b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/TimelinePathPoint.java new file mode 100644 index 00000000..696e1d44 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/importer/dto/ios/TimelinePathPoint.java @@ -0,0 +1,28 @@ +package com.dedicatedcode.reitti.service.importer.dto.ios; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TimelinePathPoint { + + @JsonProperty("point") + private String point; + + @JsonProperty("durationMinutesOffsetFromStartTime") + private String durationMinutesOffsetFromStartTime; + + public String getPoint() { + return point; + } + + public void setPoint(String point) { + this.point = point; + } + + public String getDurationMinutesOffsetFromStartTime() { + return durationMinutesOffsetFromStartTime; + } + + public void setDurationMinutesOffsetFromStartTime(String durationMinutesOffsetFromStartTime) { + this.durationMinutesOffsetFromStartTime = durationMinutesOffsetFromStartTime; + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/processing/RawLocationPointProcessingTrigger.java b/src/main/java/com/dedicatedcode/reitti/service/processing/RawLocationPointProcessingTrigger.java index 7d173c78..6daa3256 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/RawLocationPointProcessingTrigger.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/RawLocationPointProcessingTrigger.java @@ -6,6 +6,7 @@ import com.dedicatedcode.reitti.model.RawLocationPoint; import com.dedicatedcode.reitti.model.User; import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService; import com.dedicatedcode.reitti.repository.UserJdbcService; +import com.dedicatedcode.reitti.service.ImportStateHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -21,16 +22,18 @@ public class RawLocationPointProcessingTrigger { private static final Logger log = LoggerFactory.getLogger(RawLocationPointProcessingTrigger.class); private static final int BATCH_SIZE = 100; + private final ImportStateHolder stateHolder; private final RawLocationPointJdbcService rawLocationPointJdbcService; private final UserJdbcService userJdbcService; private final RabbitTemplate rabbitTemplate; private final AtomicBoolean isRunning = new AtomicBoolean(false); - public RawLocationPointProcessingTrigger(RawLocationPointJdbcService rawLocationPointJdbcService, + public RawLocationPointProcessingTrigger(ImportStateHolder stateHolder, + RawLocationPointJdbcService rawLocationPointJdbcService, UserJdbcService userJdbcService, RabbitTemplate rabbitTemplate) { - + this.stateHolder = stateHolder; this.rawLocationPointJdbcService = rawLocationPointJdbcService; this.userJdbcService = userJdbcService; this.rabbitTemplate = rabbitTemplate; @@ -40,31 +43,36 @@ public class RawLocationPointProcessingTrigger { public void start() { if (isRunning.get()) { log.warn("Processing is already running, wil skip this run"); - } else { - isRunning.set(true); - for (User user : userJdbcService.findAll()) { - List allUnprocessedPoints = rawLocationPointJdbcService.findByUserAndProcessedIsFalseOrderByTimestamp(user); - - log.debug("Found [{}] unprocessed points for user [{}]", allUnprocessedPoints.size(), user.getId()); - int i = 0; - while (i * BATCH_SIZE < allUnprocessedPoints.size()) { - int fromIndex = i * BATCH_SIZE; - int toIndex = Math.min((i + 1) * BATCH_SIZE, allUnprocessedPoints.size()); - List currentPoints = allUnprocessedPoints.subList(fromIndex, toIndex); - Instant earliest = currentPoints.getFirst().getTimestamp(); - Instant latest = currentPoints.getLast().getTimestamp(); - log.debug("Scheduling stay detection event for user [{}] and points between [{}] and [{}]", user.getId(), earliest, latest); - this.rabbitTemplate - .convertAndSend(RabbitMQConfig.EXCHANGE_NAME, - RabbitMQConfig.STAY_DETECTION_ROUTING_KEY, - new LocationProcessEvent(user.getUsername(), earliest, latest)); - currentPoints.forEach(RawLocationPoint::markProcessed); - rawLocationPointJdbcService.bulkUpdateProcessedStatus(currentPoints); - i++; - } - } - isRunning.set(false); + return; } + + if (stateHolder.isImportRunning()) { + log.warn("Data Import is currently running, wil skip this run"); + return; + } + isRunning.set(true); + for (User user : userJdbcService.findAll()) { + List allUnprocessedPoints = rawLocationPointJdbcService.findByUserAndProcessedIsFalseOrderByTimestamp(user); + + log.debug("Found [{}] unprocessed points for user [{}]", allUnprocessedPoints.size(), user.getId()); + int i = 0; + while (i * BATCH_SIZE < allUnprocessedPoints.size()) { + int fromIndex = i * BATCH_SIZE; + int toIndex = Math.min((i + 1) * BATCH_SIZE, allUnprocessedPoints.size()); + List currentPoints = allUnprocessedPoints.subList(fromIndex, toIndex); + Instant earliest = currentPoints.getFirst().getTimestamp(); + Instant latest = currentPoints.getLast().getTimestamp(); + log.debug("Scheduling stay detection event for user [{}] and points between [{}] and [{}]", user.getId(), earliest, latest); + this.rabbitTemplate + .convertAndSend(RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.STAY_DETECTION_ROUTING_KEY, + new LocationProcessEvent(user.getUsername(), earliest, latest)); + currentPoints.forEach(RawLocationPoint::markProcessed); + rawLocationPointJdbcService.bulkUpdateProcessedStatus(currentPoints); + i++; + } + } + isRunning.set(false); } } 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 87be6095..789d0915 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/TripDetectionService.java @@ -13,7 +13,6 @@ import org.springframework.stereotype.Service; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -56,8 +55,6 @@ public class TripDetectionService { List visits = this.processedVisitJdbcService.findByUserAndTimeOverlap(user, searchStart, searchEnd); - visits.sort(Comparator.comparing(ProcessedVisit::getStartTime)); - if (visits.size() < 2) { logger.info("Not enough visits to detect trips for user: {}", user.getUsername()); return; diff --git a/src/main/java/com/dedicatedcode/reitti/service/processing/VisitDetectionService.java b/src/main/java/com/dedicatedcode/reitti/service/processing/VisitDetectionService.java index 371382fb..fb91f850 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/VisitDetectionService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/VisitDetectionService.java @@ -66,68 +66,68 @@ public class VisitDetectionService { try { logger.debug("Detecting stay points for user {} from {} to {} ", username, incoming.getEarliest(), incoming.getLatest()); User user = userJdbcService.findByUsername(username).orElseThrow(); - // We extend the search window slightly to catch visits spanning midnight - Instant windowStart = incoming.getEarliest().minus(5, ChronoUnit.MINUTES); - // Get points from 1 day after the latest new point - Instant windowEnd = incoming.getLatest().plus(5, ChronoUnit.MINUTES); + // We extend the search window slightly to catch visits spanning midnight + Instant windowStart = incoming.getEarliest().minus(5, ChronoUnit.MINUTES); + // Get points from 1 day after the latest new point + Instant windowEnd = incoming.getLatest().plus(5, ChronoUnit.MINUTES); - /* - -----+++++----------+++++------+++++++---+++----------------++++++++----------------------------------------------- - ----------------------#-------------------#------------------------------------------------------------------------ - --------------------++#++------+++++++---+#+----------------------------------------------------------------------- - */ - List affectedVisits = this.visitJdbcService.findByUserAndTimeAfterAndStartTimeBefore(user, windowStart, windowEnd); - if (logger.isDebugEnabled()) { - logger.debug("Found [{}] visits which touch the timerange from [{}] to [{}]", affectedVisits.size(), windowStart, windowEnd); - affectedVisits.forEach(visit -> logger.debug("Visit [{}] from [{}] to [{}] at [{},{}]", visit.getId(), visit.getStartTime(), visit.getEndTime(), visit.getLongitude(), visit.getLatitude())); + /* + -----+++++----------+++++------+++++++---+++----------------++++++++----------------------------------------------- + ----------------------#-------------------#------------------------------------------------------------------------ + --------------------++#++------+++++++---+#+----------------------------------------------------------------------- + */ + List affectedVisits = this.visitJdbcService.findByUserAndTimeAfterAndStartTimeBefore(user, windowStart, windowEnd); + if (logger.isDebugEnabled()) { + logger.debug("Found [{}] visits which touch the timerange from [{}] to [{}]", affectedVisits.size(), windowStart, windowEnd); + affectedVisits.forEach(visit -> logger.debug("Visit [{}] from [{}] to [{}] at [{},{}]", visit.getId(), visit.getStartTime(), visit.getEndTime(), visit.getLongitude(), visit.getLatitude())); - } - try { - this.visitJdbcService.delete(affectedVisits); - logger.debug("Deleted [{}] visits with ids [{}]", affectedVisits.size(), affectedVisits.stream().map(Visit::getId).map(Object::toString).collect(Collectors.joining())); - } catch (OptimisticLockException e) { - logger.error("Optimistic lock exception", e); - throw new RuntimeException(e); - } - - if (!affectedVisits.isEmpty()) { - if (affectedVisits.getFirst().getStartTime().isBefore(windowStart)) { - windowStart = affectedVisits.getFirst().getStartTime(); + } + try { + this.visitJdbcService.delete(affectedVisits); + logger.debug("Deleted [{}] visits with ids [{}]", affectedVisits.size(), affectedVisits.stream().map(Visit::getId).map(Object::toString).collect(Collectors.joining())); + } catch (OptimisticLockException e) { + logger.error("Optimistic lock exception", e); + throw new RuntimeException(e); } - if (affectedVisits.getLast().getEndTime().isAfter(windowEnd)) { - windowEnd = affectedVisits.getLast().getEndTime(); + if (!affectedVisits.isEmpty()) { + if (affectedVisits.getFirst().getStartTime().isBefore(windowStart)) { + windowStart = affectedVisits.getFirst().getStartTime(); + } + + if (affectedVisits.getLast().getEndTime().isAfter(windowEnd)) { + windowEnd = affectedVisits.getLast().getEndTime(); + } } - } - logger.debug("Searching for points in the timerange from [{}] to [{}]", windowStart, windowEnd); + logger.debug("Searching for points in the timerange from [{}] to [{}]", windowStart, windowEnd); - double baseLatitude = affectedVisits.isEmpty() ? 50 : affectedVisits.getFirst().getLatitude(); - double[] metersAsDegrees = GeoUtils.metersToDegreesAtPosition(distanceThreshold, baseLatitude); - List clusteredPointsInTimeRangeForUser = this.rawLocationPointJdbcService.findClusteredPointsInTimeRangeForUser(user, windowStart, windowEnd, minPointsInCluster, metersAsDegrees[0]); - Map> clusteredByLocation = new HashMap<>(); - for (RawLocationPointJdbcService.ClusteredPoint clusteredPoint : clusteredPointsInTimeRangeForUser) { - if (clusteredPoint.getClusterId() != null) { - clusteredByLocation.computeIfAbsent(clusteredPoint.getClusterId(), _ -> new ArrayList<>()).add(clusteredPoint.getPoint()); + double baseLatitude = affectedVisits.isEmpty() ? 50 : affectedVisits.getFirst().getLatitude(); + double[] metersAsDegrees = GeoUtils.metersToDegreesAtPosition(distanceThreshold, baseLatitude); + List clusteredPointsInTimeRangeForUser = this.rawLocationPointJdbcService.findClusteredPointsInTimeRangeForUser(user, windowStart, windowEnd, minPointsInCluster, metersAsDegrees[0]); + Map> clusteredByLocation = new HashMap<>(); + for (RawLocationPointJdbcService.ClusteredPoint clusteredPoint : clusteredPointsInTimeRangeForUser) { + if (clusteredPoint.getClusterId() != null) { + clusteredByLocation.computeIfAbsent(clusteredPoint.getClusterId(), _ -> new ArrayList<>()).add(clusteredPoint.getPoint()); + } } - } - logger.debug("Found {} point clusters in the processing window from [{}] to [{}]", clusteredByLocation.size(), windowStart, windowEnd); + logger.debug("Found {} point clusters in the processing window from [{}] to [{}]", clusteredByLocation.size(), windowStart, windowEnd); - // Apply the stay point detection algorithm - List stayPoints = detectStayPointsFromTrajectory(clusteredByLocation); + // Apply the stay point detection algorithm + List stayPoints = detectStayPointsFromTrajectory(clusteredByLocation); - logger.info("Detected {} stay points for user {}", stayPoints.size(), user.getUsername()); + logger.info("Detected {} stay points for user {}", stayPoints.size(), user.getUsername()); - List createdVisits = new ArrayList<>(); + List createdVisits = new ArrayList<>(); - for (StayPoint stayPoint : stayPoints) { - Visit visit = createVisit(stayPoint.getLongitude(), stayPoint.getLatitude(), stayPoint); - logger.debug("Creating new visit: {}", visit); - createdVisits.add(visit); - } + for (StayPoint stayPoint : stayPoints) { + Visit visit = createVisit(stayPoint.getLongitude(), stayPoint.getLatitude(), stayPoint); + logger.debug("Creating new visit: {}", visit); + createdVisits.add(visit); + } - List createdIds = visitJdbcService.bulkInsert(user, createdVisits).stream().map(Visit::getId).toList(); - rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.MERGE_VISIT_ROUTING_KEY, new VisitUpdatedEvent(user.getUsername(), createdIds)); + List createdIds = visitJdbcService.bulkInsert(user, createdVisits).stream().map(Visit::getId).toList(); + rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.MERGE_VISIT_ROUTING_KEY, new VisitUpdatedEvent(user.getUsername(), createdIds)); } 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 d1c0a0af..fd3228d1 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/processing/VisitMergingService.java @@ -64,20 +64,21 @@ public class VisitMergingService { } public void visitUpdated(VisitUpdatedEvent event) { - handleEvent(event.getUsername(), event.getVisitIds()); + String username = event.getUsername(); + handleEvent(username, event.getVisitIds()); } private void handleEvent(String username, List visitIds) { - Optional user = userJdbcService.findByUsername(username); - if (user.isEmpty()) { - logger.warn("User not found for userName: {}", username); - return; - } - List visits = this.visitJdbcService.findAllByIds(visitIds); - if (visits.isEmpty()) { - logger.debug("Visit not found for visitId: [{}]", visitIds); - return; - } + Optional user = userJdbcService.findByUsername(username); + if (user.isEmpty()) { + logger.warn("User not found for userName: {}", username); + return; + } + List visits = this.visitJdbcService.findAllByIds(visitIds); + if (visits.isEmpty()) { + logger.debug("Visit not found for visitId: [{}]", visitIds); + return; + } Instant searchStart = visits.stream().min(Comparator.comparing(Visit::getStartTime)).map(Visit::getStartTime).map(instant -> instant.minus(searchRangeExtensionInHours, ChronoUnit.HOURS)).orElseThrow(); Instant searchEnd = visits.stream().max(Comparator.comparing(Visit::getEndTime)).map(Visit::getEndTime).map(instant -> instant.plus(searchRangeExtensionInHours, ChronoUnit.HOURS)).orElseThrow(); diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 8423ecd0..49935ab3 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -5,4 +5,4 @@ logging.level.com.dedicatedcode = INFO reitti.geocoding.photon.base-url=http://localhost:2322 -reitti.events.concurrency=1-4 +reitti.events.concurrency=4-12 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 94e90f07..5d2b0afb 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -139,6 +139,10 @@ upload.google.new.format.title=Google Timeline New Format (timeline.json) upload.google.new.format.instructions=From your Android phone: Settings \u2192 Location \u2192 Location Services \u2192 Timeline \u2192 Export Timeline upload.google.new.format.description=This exports a timeline.json file with your recent location data. upload.google.new.format.ios.instructions=From iOS Google Maps: Open Google Maps \u2192 Click on your Profile \u2192 Settings \u2192 Personal content \u2192 Export Timeline Data +upload.google.android.format.title=Android Timeline (timeline.json) +upload.google.android.format.description=This exports a timeline.json file with your recent location data from Android devices. +upload.google.ios.format.title=iOS Timeline (timeline.json) +upload.google.ios.format.description=This exports a timeline.json file with your recent location data from iOS devices. upload.google.old.format.title=Google Timeline Old Format (Records.json) upload.google.old.format.instructions=Export your data from takeout.google.com and upload the Records.json file from the Location History folder. upload.google.old.format.description=This contains your complete historical location data. @@ -147,6 +151,8 @@ upload.geojson.description=Upload GeoJSON files containing Point features with l upload.button.gpx=Upload GPX File upload.button.google=Upload Google Takeout upload.button.google.timeline=Upload Timeline Data +upload.button.google.timeline.android=Upload Android Timeline Data +upload.button.google.timeline.ios=Upload iOS Timeline Data upload.button.google.records=Upload Records Data upload.button.geojson=Upload GeoJSON File upload.no.files=No files selected diff --git a/src/main/resources/messages_de.properties b/src/main/resources/messages_de.properties index 86c234ca..4d2b6640 100644 --- a/src/main/resources/messages_de.properties +++ b/src/main/resources/messages_de.properties @@ -148,7 +148,11 @@ upload.google.new.format.title=Google Zeitleiste neues Format (timeline.json) upload.google.new.format.instructions=Von Ihrem Android-Telefon: Einstellungen \u2192 Standort \u2192 Standortdienste \u2192 Zeitachse \u2192 Zeitachse exportieren upload.google.new.format.description=Dies exportiert eine timeline.json-Datei mit Ihren aktuellen Standortdaten. upload.google.new.format.ios.instructions=Von iOS Google Maps: Google Maps \u00F6ffnen \u2192 Auf Ihr Profil klicken \u2192 Einstellungen \u2192 Pers\u00F6nliche Inhalte \u2192 Zeitleisten-Daten exportieren -upload.google.old.format.title=Google Zeitleist altes Format (Records.json) +upload.google.android.format.title=Android Timeline (timeline.json) +upload.google.android.format.description=Dies exportiert eine timeline.json-Datei mit Ihren aktuellen Standortdaten von Android-Ger\u00E4ten. +upload.google.ios.format.title=iOS Timeline (timeline.json) +upload.google.ios.format.description=Dies exportiert eine timeline.json-Datei mit Ihren aktuellen Standortdaten von iOS-Ger\u00E4ten. +upload.google.old.format.title=Google Zeitleiste altes Format (Records.json) upload.google.old.format.instructions=Von Google Takeout: Exportieren Sie Ihre Daten von takeout.google.com und laden Sie die Records.json-Datei aus dem Zeitlachse-Ordner hoch. upload.google.old.format.description=Dies enth\u00E4lt Ihre vollst\u00E4ndigen historischen Standortdaten. upload.geojson.title=GeoJSON-Dateien @@ -156,6 +160,8 @@ upload.geojson.description=Laden Sie GeoJSON-Dateien mit Point-Features und Stan upload.button.gpx=GPX-Datei hochladen upload.button.google=Google Takeout hochladen upload.button.google.timeline=Timeline-Daten hochladen +upload.button.google.timeline.android=Android Timeline-Daten hochladen +upload.button.google.timeline.ios=iOS Timeline-Daten hochladen upload.button.google.records=Records-Daten hochladen upload.button.geojson=GeoJSON-Datei hochladen upload.no.files=Keine Dateien ausgew\u00E4hlt diff --git a/src/main/resources/messages_fi.properties b/src/main/resources/messages_fi.properties index 757ace81..428e36e6 100644 --- a/src/main/resources/messages_fi.properties +++ b/src/main/resources/messages_fi.properties @@ -139,6 +139,10 @@ upload.google.new.format.title=📱 Uusi muoto (timeline.json) upload.google.new.format.instructions=Android-puhelimestasi: Asetukset → Sijainti → Sijaintipalvelut → Aikajana → Vie aikajana upload.google.new.format.description=Tämä vie timeline.json -tiedoston viimeaikaisine sijaintitietoinesi. upload.google.new.format.ios.instructions=iOS Google Mapsista: Avaa Google Maps → Klikkaa profiiliasi → Asetukset → Henkilökohtainen sisältö → Vie aikajanan tiedot +upload.google.android.format.title=Android Timeline (timeline.json) +upload.google.android.format.description=Tämä vie timeline.json-tiedoston viimeaikaisilla sijaintitiedoillasi Android-laitteista. +upload.google.ios.format.title=iOS Timeline (timeline.json) +upload.google.ios.format.description=Tämä vie timeline.json-tiedoston viimeaikaisilla sijaintitiedoillasi iOS-laitteista. upload.google.old.format.title=🌐 Vanha muoto (Records.json) upload.google.old.format.instructions=Google Takeoutista: Vie tietosi osoitteesta takeout.google.com ja lataa Records.json -tiedosto Location History -kansiosta. upload.google.old.format.description=Tämä sisältää täydellisen historiallisen sijaintitietosi. @@ -147,6 +151,8 @@ upload.geojson.description=Lataa GeoJSON-tiedostoja, jotka sis\u00E4lt\u00E4v\u0 upload.button.gpx=Lataa GPX-tiedosto upload.button.google=Lataa Google Takeout upload.button.google.timeline=Lataa aikajana-tiedot +upload.button.google.timeline.android=Lataa Android aikajana-tiedot +upload.button.google.timeline.ios=Lataa iOS aikajana-tiedot upload.button.google.records=Lataa Records-tiedot upload.button.geojson=Lataa GeoJSON-tiedosto upload.no.files=Tiedostoja ei valittu diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties index 780effc7..f6edab8f 100644 --- a/src/main/resources/messages_fr.properties +++ b/src/main/resources/messages_fr.properties @@ -149,6 +149,10 @@ upload.google.new.format.android.instructions=Depuis votre t\u00E9l\u00E9phone A upload.google.new.format.ios.instructions=Depuis iOS Google Maps : Ouvrir Google Maps \u2192 Cliquer sur votre Profil \u2192 Param\u00E8tres \u2192 Contenu personnel \u2192 Exporter les donn\u00E9es de timeline upload.google.new.format.description=Ceci exporte un fichier timeline.json avec vos donn\u00E9es de localisation r\u00E9centes. upload.google.new.format.notice=Les fichiers timeline.json export\u00E9s depuis Google Maps sur iOS ne sont actuellement pas pris en charge. Ces fichiers manquent des donn\u00E9es brutes que Reitti n\u00E9cessite pour ses calculs. +upload.google.android.format.title=Android Timeline (timeline.json) +upload.google.android.format.description=Ceci exporte un fichier timeline.json avec vos donn\u00E9es de localisation r\u00E9centes depuis les appareils Android. +upload.google.ios.format.title=iOS Timeline (timeline.json) +upload.google.ios.format.description=Ceci exporte un fichier timeline.json avec vos donn\u00E9es de localisation r\u00E9centes depuis les appareils iOS. upload.google.old.format.title=\uD83C\uDF10 Ancien format (Records.json) upload.google.old.format.instructions=Depuis Google Takeout : Exportez vos donn\u00E9es depuis takeout.google.com et t\u00E9l\u00E9chargez le fichier Records.json du dossier Location History. upload.google.old.format.description=Ceci contient vos donn\u00E9es de localisation historiques compl\u00E8tes. @@ -157,6 +161,8 @@ upload.geojson.description=T\u00E9l\u00E9chargez des fichiers GeoJSON contenant upload.button.gpx=T\u00E9l\u00E9charger un fichier GPX upload.button.google=T\u00E9l\u00E9charger Google Takeout upload.button.google.timeline=T\u00E9l\u00E9charger les donn\u00E9es Timeline +upload.button.google.timeline.android=T\u00E9l\u00E9charger les donn\u00E9es Timeline Android +upload.button.google.timeline.ios=T\u00E9l\u00E9charger les donn\u00E9es Timeline iOS upload.button.google.records=T\u00E9l\u00E9charger les donn\u00E9es Records upload.button.geojson=T\u00E9l\u00E9charger un fichier GeoJSON upload.no.files=Aucun fichier s\u00E9lectionn\u00E9 diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index fe14947d..c0ae4fce 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -1137,3 +1137,34 @@ button:disabled { cursor: not-allowed; color: white; } + +.sr-only { + display: none; +} + +.spinner { + display: none; + align-items: center; + gap: 10px; + margin-top: 10px; +} + +.spinner-border { + width: 1rem; + height: 1rem; + border: 0.125em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spinner-border 0.75s linear infinite; +} + +@keyframes spinner-border { + to { + transform: rotate(360deg); + } +} + +.upload-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/main/resources/templates/fragments/file-upload.html b/src/main/resources/templates/fragments/file-upload.html index c5a77c6b..db5410b8 100644 --- a/src/main/resources/templates/fragments/file-upload.html +++ b/src/main/resources/templates/fragments/file-upload.html @@ -21,21 +21,21 @@ tracks, and routes with timestamps that can be processed into your location history.

- + +
-

GeoJSON Files

@@ -45,71 +45,94 @@ Supports both single Feature and FeatureCollection formats.

- + +
-
-

📱 New Format (timeline.json)

+

📱 Android Timeline (timeline.json)

Android: From your Android phone: Settings → Location → Location Services → Timeline → Export Timeline

-

iOS: From iOS Google Maps: Open Google Maps → Click on your Profile → Settings → Personal content → Export Timeline Data

-

This exports a timeline.json file with your recent location data.

+

This exports a timeline.json file with your recent location data from Android devices.

-
- + +
- - + +
+
+

📱 iOS Timeline (timeline.json)

+

iOS: From iOS Google Maps: Open Google Maps → Click on your Profile → Settings → Personal content → Export Timeline Data

+

This exports a timeline.json file with your recent location data from iOS devices.

+ +
+
+ +
+ + +
+

🌐 Old Format (Records.json)

From Google Takeout: Export your data from takeout.google.com and upload the Records.json file from the Location History folder.

This contains your complete historical location data.

- + +
-
+ diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html index 677b7667..c9f4e759 100644 --- a/src/main/resources/templates/settings.html +++ b/src/main/resources/templates/settings.html @@ -212,6 +212,67 @@ }); }); }); + + document.addEventListener('DOMContentLoaded', function() { + // Function to disable all upload buttons and show spinners + function disableAllUploads() { + const uploadButtons = document.querySelectorAll('.upload-btn'); + const spinners = document.querySelectorAll('.spinner'); + + uploadButtons.forEach(button => { + button.disabled = true; + button.style.display = 'none'; + }); + + spinners.forEach(spinner => { + spinner.style.display = 'flex'; + }); + } + + // Function to enable all upload buttons and hide spinners + function enableAllUploads() { + const uploadButtons = document.querySelectorAll('.upload-btn'); + const spinners = document.querySelectorAll('.spinner'); + + uploadButtons.forEach(button => { + button.disabled = false; + button.style.display = 'inline-block'; + }); + + spinners.forEach(spinner => { + spinner.style.display = 'none'; + }); + } + + // Listen for HTMX request start events on all upload forms + document.body.addEventListener('htmx:beforeRequest', function(evt) { + const form = evt.target; + if (form.id && form.id.includes('upload-form')) { + disableAllUploads(); + } + }); + + // Listen for HTMX request completion events + document.body.addEventListener('htmx:afterRequest', function(evt) { + const form = evt.target; + if (form.id && form.id.includes('upload-form')) { + enableAllUploads(); + } + }); + + // Handle progress events + document.body.addEventListener('htmx:xhr:progress', function(evt) { + const form = evt.target; + if (form.id) { + const progressId = form.id.replace('-upload-form', '').replace('-', '-'); + const progressElement = document.getElementById('progress-' + progressId); + if (progressElement) { + progressElement.setAttribute('value', evt.detail.loaded/evt.detail.total * 100); + progressElement.style.display = 'block'; + } + } + }); + }); diff --git a/src/test/java/com/dedicatedcode/reitti/TestingService.java b/src/test/java/com/dedicatedcode/reitti/TestingService.java index 07217418..c3dae5e3 100644 --- a/src/test/java/com/dedicatedcode/reitti/TestingService.java +++ b/src/test/java/com/dedicatedcode/reitti/TestingService.java @@ -3,7 +3,8 @@ package com.dedicatedcode.reitti; import com.dedicatedcode.reitti.config.RabbitMQConfig; import com.dedicatedcode.reitti.model.User; import com.dedicatedcode.reitti.repository.*; -import com.dedicatedcode.reitti.service.ImportHandler; +import com.dedicatedcode.reitti.service.importer.GeoJsonImporter; +import com.dedicatedcode.reitti.service.importer.GpxImporter; import com.dedicatedcode.reitti.service.processing.RawLocationPointProcessingTrigger; import org.awaitility.Awaitility; import org.springframework.amqp.rabbit.core.RabbitAdmin; @@ -32,7 +33,9 @@ public class TestingService { @Autowired private UserJdbcService userJdbcService; @Autowired - private ImportHandler importHandler; + private GpxImporter gpxImporter; + @Autowired + private GeoJsonImporter geoJsonImporter; @Autowired private RawLocationPointJdbcService rawLocationPointRepository; @Autowired @@ -51,9 +54,9 @@ public class TestingService { .orElseThrow(() -> new UsernameNotFoundException("User not found: " + (Long) 1L)); InputStream is = getClass().getResourceAsStream(path); if (path.endsWith(".gpx")) { - importHandler.importGpx(is, admin); + gpxImporter.importGpx(is, admin); } else if (path.endsWith(".geojson")) { - importHandler.importGeoJson(is, admin); + geoJsonImporter.importGeoJson(is, admin); } else { throw new IllegalStateException("Unsupported file type: " + path); } @@ -63,9 +66,6 @@ public class TestingService { return this.userJdbcService.findById(1L) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + (Long) 1L)); } - public void triggerProcessingPipeline() { - trigger.start(); - } public void triggerProcessingPipeline(int timeoout) { trigger.start(); diff --git a/src/test/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporterTest.java b/src/test/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporterTest.java new file mode 100644 index 00000000..ddecc313 --- /dev/null +++ b/src/test/java/com/dedicatedcode/reitti/service/importer/BaseGoogleTimelineImporterTest.java @@ -0,0 +1,67 @@ +package com.dedicatedcode.reitti.service.importer; + +import com.dedicatedcode.reitti.IntegrationTest; +import com.dedicatedcode.reitti.TestingService; +import com.dedicatedcode.reitti.model.ProcessedVisit; +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.repository.ProcessedVisitJdbcService; +import com.dedicatedcode.reitti.service.processing.RawLocationPointProcessingTrigger; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@IntegrationTest +class BaseGoogleTimelineImporterTest { + + @Autowired + private TestingService testingService; + + @Autowired + private GoogleAndroidTimelineImporter googleTimelineImporter; + + @Autowired + private ProcessedVisitJdbcService visitJdbcService; + + @Autowired + private RawLocationPointProcessingTrigger trigger; + @Test + void shouldParseNewGoogleTakeOutFileFromAndroid() { + User user = testingService.admin(); + Map result = googleTimelineImporter.importTimeline(getClass().getResourceAsStream("/data/google/timeline_from_android_randomized.json"), user); + + assertTrue(result.containsKey("success")); + assertTrue((Boolean) result.get("success")); + + testingService.awaitDataImport(20); + trigger.start(); + testingService.awaitDataImport(20); + + List createdVisits = this.visitJdbcService.findByUser(user); + assertEquals(6, createdVisits.size()); + //"startTime" : "2017-05-02T12:12:04+10:00", + //"endTime" : "2017-05-02T18:52:12+10:00", + + //"startTime" : "2017-05-02T19:16:01+10:00", + // "endTime" : "2017-05-02T20:48:52+10:00", + + // "startTime" : "2017-05-02T21:17:03+10:00", + // "endTime" : "2017-05-03T14:23:20+10:00", + + // "startTime" : "2017-05-03T15:10:14+10:00", + // "endTime" : "2017-05-03T23:50:01+10:00", + + // "startTime" : "2017-05-04T00:05:33+10:00", + // "endTime" : "2017-05-04T00:17:23+10:00", + + //"startTime" : "2017-05-04T00:08:21+10:00", + // "endTime" : "2017-05-04T00:17:23+10:00", + + //"startTime" : "2017-05-04T00:44:27+10:00", + // "endTime" : "2017-05-04T14:51:51+10:00", + } +} diff --git a/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporterTest.java b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporterTest.java new file mode 100644 index 00000000..574ee0d8 --- /dev/null +++ b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleAndroidTimelineImporterTest.java @@ -0,0 +1,47 @@ +package com.dedicatedcode.reitti.service.importer; + +import com.dedicatedcode.reitti.config.RabbitMQConfig; +import com.dedicatedcode.reitti.event.LocationDataEvent; +import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.amqp.rabbit.core.RabbitTemplate; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class GoogleAndroidTimelineImporterTest { + + @Test + void shouldParseNewGoogleTakeOutFileFromAndroid() { + RabbitTemplate mock = mock(RabbitTemplate.class); + GoogleAndroidTimelineImporter importHandler = new GoogleAndroidTimelineImporter(new ObjectMapper(), new ImportStateHolder(), new ImportBatchProcessor(mock, 100), 5, 100, 300); + User user = new User("test", "Test User"); + Map result = importHandler.importTimeline(getClass().getResourceAsStream("/data/google/timeline_from_android_randomized.json"), user); + + assertTrue(result.containsKey("success")); + assertTrue((Boolean) result.get("success")); + + // Create a spy to retrieve all LocationDataEvents pushed into RabbitMQ + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(LocationDataEvent.class); + verify(mock, times(30)).convertAndSend(eq(RabbitMQConfig.EXCHANGE_NAME), eq(RabbitMQConfig.LOCATION_DATA_ROUTING_KEY), eventCaptor.capture()); + + List capturedEvents = eventCaptor.getAllValues(); + assertEquals(30, capturedEvents.size()); + + // Verify that all events are for the correct user + for (LocationDataEvent event : capturedEvents) { + assertEquals("test", event.getUsername()); + assertNotNull(event.getPoints()); + assertFalse(event.getPoints().isEmpty()); + + event.getPoints().forEach(point -> assertNotNull(point.getAccuracyMeters())); + } + } +} diff --git a/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporterTest.java b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporterTest.java similarity index 67% rename from src/test/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporterTest.java rename to src/test/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporterTest.java index fd4e26e3..6fc9c2ae 100644 --- a/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleTimelineImporterTest.java +++ b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleIOSTimelineImporterTest.java @@ -3,6 +3,7 @@ package com.dedicatedcode.reitti.service.importer; import com.dedicatedcode.reitti.config.RabbitMQConfig; import com.dedicatedcode.reitti.event.LocationDataEvent; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -15,26 +16,25 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -class GoogleTimelineImporterTest { - +class GoogleIOSTimelineImporterTest { @Test - void shouldParseNewGoogleTakeOutFile() { + void shouldParseNewGoogleTakeOutFileFromIOS() { RabbitTemplate mock = mock(RabbitTemplate.class); - GoogleTimelineImporter importHandler = new GoogleTimelineImporter(new ObjectMapper(), new ImportBatchProcessor(mock, 100), 5, 100); + GoogleIOSTimelineImporter importHandler = new GoogleIOSTimelineImporter(new ObjectMapper(), new ImportStateHolder(), new ImportBatchProcessor(mock, 100), 5, 100, 300); User user = new User("test", "Test User"); - Map result = importHandler.importGoogleTimeline(getClass().getResourceAsStream("/data/google/tl_randomized.json"), user); + Map result = importHandler.importTimeline(getClass().getResourceAsStream("/data/google/timeline_from_ios_randomized.json"), user); assertTrue(result.containsKey("success")); assertTrue((Boolean) result.get("success")); // Create a spy to retrieve all LocationDataEvents pushed into RabbitMQ ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(LocationDataEvent.class); - verify(mock, times(5)).convertAndSend(eq(RabbitMQConfig.EXCHANGE_NAME), eq(RabbitMQConfig.LOCATION_DATA_ROUTING_KEY), eventCaptor.capture()); - + verify(mock, times(118)).convertAndSend(eq(RabbitMQConfig.EXCHANGE_NAME), eq(RabbitMQConfig.LOCATION_DATA_ROUTING_KEY), eventCaptor.capture()); + List capturedEvents = eventCaptor.getAllValues(); - assertEquals(5, capturedEvents.size()); - + assertEquals(118, capturedEvents.size()); + // Verify that all events are for the correct user for (LocationDataEvent event : capturedEvents) { assertEquals("test", event.getUsername()); @@ -43,5 +43,6 @@ class GoogleTimelineImporterTest { event.getPoints().forEach(point -> assertNotNull(point.getAccuracyMeters())); } + } -} +} \ No newline at end of file diff --git a/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporterTest.java b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporterTest.java index 43bf833c..aa71be27 100644 --- a/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporterTest.java +++ b/src/test/java/com/dedicatedcode/reitti/service/importer/GoogleRecordsImporterTest.java @@ -3,6 +3,7 @@ package com.dedicatedcode.reitti.service.importer; import com.dedicatedcode.reitti.config.RabbitMQConfig; import com.dedicatedcode.reitti.event.LocationDataEvent; import com.dedicatedcode.reitti.model.User; +import com.dedicatedcode.reitti.service.ImportStateHolder; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -19,7 +20,7 @@ class GoogleRecordsImporterTest { @Test void shouldParseOldFormat() { RabbitTemplate mock = mock(RabbitTemplate.class); - GoogleRecordsImporter importHandler = new GoogleRecordsImporter(new ObjectMapper(), new ImportBatchProcessor(mock, 100)); + GoogleRecordsImporter importHandler = new GoogleRecordsImporter(new ObjectMapper(), new ImportStateHolder(), new ImportBatchProcessor(mock, 100)); User user = new User("test", "Test User"); Map result = importHandler.importGoogleRecords(getClass().getResourceAsStream("/data/google/Records.json"), user); diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index af547e05..bb598d11 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -13,4 +13,4 @@ reitti.process-data.schedule=- reitti.imports.schedule=- logging.level.root = INFO -logging.level.com.dedicatedcode.reitti = DEBUG +logging.level.com.dedicatedcode.reitti = TRACE diff --git a/src/test/resources/data/google/Records.json b/src/test/resources/data/google/Records.json index 83437216..c2c8d23c 100644 --- a/src/test/resources/data/google/Records.json +++ b/src/test/resources/data/google/Records.json @@ -994,4 +994,5 @@ "deviceTag": 335552189, "timestamp": "2013-04-15T08:53:31.548Z" } -] + ] +} diff --git a/src/test/resources/data/google/Zeitachse.json b/src/test/resources/data/google/Zeitachse.json deleted file mode 100644 index 7cb00bd2..00000000 --- a/src/test/resources/data/google/Zeitachse.json +++ /dev/null @@ -1,8238 +0,0 @@ -{ - "semanticSegments": [ - { - "startTime": "2025-07-04T09:00:00.000+02:00", - "endTime": "2025-07-04T11:00:00.000+02:00", - "timelinePath": [ - { - "point": "53.86324°, 10.7011764°", - "time": "2025-07-04T09:38:00.000+02:00" - } - ] - } - ], - "rawSignals": [ - { - "position": { - "LatLng": "53.8633043°, 10.7011529°", - "accuracyMeters": 3, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-03T19:46:13.000+02:00", - "speedMetersPerSecond": 0.00535565847530961 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:37:34.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -92 - }, - { - "mac": 4880535074783, - "rawRssi": -71 - }, - { - "mac": 176353769414806, - "rawRssi": -78 - }, - { - "mac": 4880535074782, - "rawRssi": -64 - }, - { - "mac": 123486069126043, - "rawRssi": -76 - }, - { - "mac": 79565160561131, - "rawRssi": -93 - }, - { - "mac": 118796079720199, - "rawRssi": -82 - }, - { - "mac": 176156956989012, - "rawRssi": -60 - }, - { - "mac": 176156956989009, - "rawRssi": -63 - }, - { - "mac": 233833088632553, - "rawRssi": -82 - }, - { - "mac": 13685719791895, - "rawRssi": -86 - }, - { - "mac": 5175406356058, - "rawRssi": -76 - }, - { - "mac": 103893075959292, - "rawRssi": -85 - }, - { - "mac": 79565174465541, - "rawRssi": -93 - }, - { - "mac": 207775459763230, - "rawRssi": -61 - }, - { - "mac": 272875381228876, - "rawRssi": -71 - }, - { - "mac": 236032111888024, - "rawRssi": -68 - }, - { - "mac": 207775459763231, - "rawRssi": -73 - }, - { - "mac": 251751037760296, - "rawRssi": -69 - }, - { - "mac": 251751037760295, - "rawRssi": -58 - }, - { - "mac": 253950061015831, - "rawRssi": -61 - }, - { - "mac": 176353769414803, - "rawRssi": -70 - }, - { - "mac": 13685728359545, - "rawRssi": -80 - }, - { - "mac": 253950061015863, - "rawRssi": -60 - }, - { - "mac": 176156956988992, - "rawRssi": -44 - }, - { - "mac": 162987945711694, - "rawRssi": -78 - }, - { - "mac": 66207235812212, - "rawRssi": -93 - }, - { - "mac": 176156956988996, - "rawRssi": -36 - }, - { - "mac": 245000064254810, - "rawRssi": -73 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9700000286102295 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:37:37.000+02:00" - } - }, - { - "position": { - "LatLng": "53.86324°, 10.7011764°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:37:38.000+02:00", - "speedMetersPerSecond": 0.27601006627082825 - } - }, - { - "position": { - "LatLng": "53.863234°, 10.7011818°", - "accuracyMeters": 8, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:37:41.000+02:00", - "speedMetersPerSecond": 0.2278139591217041 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:37:42.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:37:47.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:37:52.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:37:54.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -92 - }, - { - "mac": 4880535074783, - "rawRssi": -68 - }, - { - "mac": 248525417280042, - "rawRssi": -91 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -57 - }, - { - "mac": 123486069126043, - "rawRssi": -73 - }, - { - "mac": 79565160561131, - "rawRssi": -94 - }, - { - "mac": 13685728359546, - "rawRssi": -89 - }, - { - "mac": 13685723516731, - "rawRssi": -86 - }, - { - "mac": 176156956989012, - "rawRssi": -59 - }, - { - "mac": 4880535074786, - "rawRssi": -68 - }, - { - "mac": 176156956989009, - "rawRssi": -63 - }, - { - "mac": 233833088632553, - "rawRssi": -82 - }, - { - "mac": 13685719791895, - "rawRssi": -86 - }, - { - "mac": 5175406356058, - "rawRssi": -78 - }, - { - "mac": 246326394024490, - "rawRssi": -93 - }, - { - "mac": 196031866815400, - "rawRssi": -68 - }, - { - "mac": 79565174465541, - "rawRssi": -91 - }, - { - "mac": 207775459763230, - "rawRssi": -67 - }, - { - "mac": 207775459763231, - "rawRssi": -67 - }, - { - "mac": 272875381228877, - "rawRssi": -84 - }, - { - "mac": 251751037760296, - "rawRssi": -68 - }, - { - "mac": 251751037760295, - "rawRssi": -61 - }, - { - "mac": 253950061015831, - "rawRssi": -58 - }, - { - "mac": 176353769414803, - "rawRssi": -72 - }, - { - "mac": 253950061015863, - "rawRssi": -60 - }, - { - "mac": 225185649798751, - "rawRssi": -82 - }, - { - "mac": 176156956988992, - "rawRssi": -50 - }, - { - "mac": 162987945711694, - "rawRssi": -77 - }, - { - "mac": 176156956988996, - "rawRssi": -32 - }, - { - "mac": 245000064254810, - "rawRssi": -70 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:37:57.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9700000286102295 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:38:02.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:07.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863225°, 10.7011396°", - "accuracyMeters": 3, - "altitudeMeters": 52.89999771118164, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:38:11.000+02:00", - "speedMetersPerSecond": 0.0057569569908082485 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:12.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:17.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:22.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:27.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:38:29.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -92 - }, - { - "mac": 248525417280043, - "rawRssi": -83 - }, - { - "mac": 4880535074783, - "rawRssi": -69 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -62 - }, - { - "mac": 79565160561131, - "rawRssi": -89 - }, - { - "mac": 13685728359546, - "rawRssi": -91 - }, - { - "mac": 13685719791891, - "rawRssi": -80 - }, - { - "mac": 176156956989012, - "rawRssi": -55 - }, - { - "mac": 31031826868571, - "rawRssi": -76 - }, - { - "mac": 176156956989009, - "rawRssi": -61 - }, - { - "mac": 236032111888056, - "rawRssi": -69 - }, - { - "mac": 5175406356059, - "rawRssi": -80 - }, - { - "mac": 13685719791895, - "rawRssi": -87 - }, - { - "mac": 88949690804935, - "rawRssi": -82 - }, - { - "mac": 246326394024491, - "rawRssi": -84 - }, - { - "mac": 196031866815400, - "rawRssi": -66 - }, - { - "mac": 66692969804511, - "rawRssi": -85 - }, - { - "mac": 79565174465541, - "rawRssi": -91 - }, - { - "mac": 207775459763230, - "rawRssi": -65 - }, - { - "mac": 207775459763231, - "rawRssi": -70 - }, - { - "mac": 272875381228877, - "rawRssi": -82 - }, - { - "mac": 251751037760296, - "rawRssi": -69 - }, - { - "mac": 251751037760295, - "rawRssi": -58 - }, - { - "mac": 253950061015831, - "rawRssi": -59 - }, - { - "mac": 176353769414803, - "rawRssi": -75 - }, - { - "mac": 253950061015863, - "rawRssi": -61 - }, - { - "mac": 176156956988992, - "rawRssi": -48 - }, - { - "mac": 66207235812212, - "rawRssi": -90 - }, - { - "mac": 176156956988996, - "rawRssi": -27 - }, - { - "mac": 245000064254810, - "rawRssi": -70 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:32.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:37.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632397°, 10.7011374°", - "accuracyMeters": 9, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:38:41.000+02:00", - "speedMetersPerSecond": 0.00972543191164732 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:42.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:38:44.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -90 - }, - { - "mac": 4880535074783, - "rawRssi": -68 - }, - { - "mac": 248525417280042, - "rawRssi": -92 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -61 - }, - { - "mac": 123486069126043, - "rawRssi": -77 - }, - { - "mac": 49004153403042, - "rawRssi": -92 - }, - { - "mac": 13685719791891, - "rawRssi": -79 - }, - { - "mac": 233254798443902, - "rawRssi": -92 - }, - { - "mac": 176156956989012, - "rawRssi": -52 - }, - { - "mac": 178355980244552, - "rawRssi": -27 - }, - { - "mac": 31031826868571, - "rawRssi": -76 - }, - { - "mac": 101694052703736, - "rawRssi": -85 - }, - { - "mac": 176156956989009, - "rawRssi": -64 - }, - { - "mac": 5175406356059, - "rawRssi": -73 - }, - { - "mac": 233833088632553, - "rawRssi": -86 - }, - { - "mac": 233833088632552, - "rawRssi": -72 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -74 - }, - { - "mac": 103893075959290, - "rawRssi": -85 - }, - { - "mac": 79565174465541, - "rawRssi": -90 - }, - { - "mac": 207775459763230, - "rawRssi": -63 - }, - { - "mac": 207775459763231, - "rawRssi": -70 - }, - { - "mac": 272875381228877, - "rawRssi": -83 - }, - { - "mac": 187152073266777, - "rawRssi": -52 - }, - { - "mac": 251751037760296, - "rawRssi": -70 - }, - { - "mac": 251751037760295, - "rawRssi": -59 - }, - { - "mac": 253950061015831, - "rawRssi": -60 - }, - { - "mac": 176353769414803, - "rawRssi": -74 - }, - { - "mac": 253950061015863, - "rawRssi": -65 - }, - { - "mac": 176156956988992, - "rawRssi": -49 - }, - { - "mac": 66207235812212, - "rawRssi": -91 - }, - { - "mac": 176156956988996, - "rawRssi": -29 - }, - { - "mac": 245000064254810, - "rawRssi": -74 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:47.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:38:52.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:10.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863247°, 10.7011363°", - "accuracyMeters": 6, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:39:12.000+02:00", - "speedMetersPerSecond": 0.049255672842264175 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:15.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:20.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:25.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:39:27.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -90 - }, - { - "mac": 4880535074783, - "rawRssi": -69 - }, - { - "mac": 176353769414806, - "rawRssi": -76 - }, - { - "mac": 4880535074782, - "rawRssi": -62 - }, - { - "mac": 123486069126043, - "rawRssi": -78 - }, - { - "mac": 79565160561131, - "rawRssi": -90 - }, - { - "mac": 13685719791891, - "rawRssi": -81 - }, - { - "mac": 176156956989012, - "rawRssi": -53 - }, - { - "mac": 31031826868571, - "rawRssi": -76 - }, - { - "mac": 4880535074786, - "rawRssi": -68 - }, - { - "mac": 176156956989009, - "rawRssi": -61 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -75 - }, - { - "mac": 88949690804935, - "rawRssi": -83 - }, - { - "mac": 79565174465541, - "rawRssi": -89 - }, - { - "mac": 207775459763230, - "rawRssi": -63 - }, - { - "mac": 272875381228876, - "rawRssi": -69 - }, - { - "mac": 236032111888024, - "rawRssi": -66 - }, - { - "mac": 207775459763231, - "rawRssi": -72 - }, - { - "mac": 272875381228877, - "rawRssi": -83 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -65 - }, - { - "mac": 253950061015831, - "rawRssi": -65 - }, - { - "mac": 176353769414803, - "rawRssi": -73 - }, - { - "mac": 13685728359545, - "rawRssi": -78 - }, - { - "mac": 110827922517988, - "rawRssi": -81 - }, - { - "mac": 253950061015863, - "rawRssi": -64 - }, - { - "mac": 225185649798751, - "rawRssi": -81 - }, - { - "mac": 176156956988992, - "rawRssi": -49 - }, - { - "mac": 66207235812212, - "rawRssi": -91 - }, - { - "mac": 176156956988996, - "rawRssi": -27 - }, - { - "mac": 245000064254810, - "rawRssi": -70 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:31.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:36.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:41.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632501°, 10.7011262°", - "accuracyMeters": 3, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:39:42.000+02:00", - "speedMetersPerSecond": 0.04154004901647568 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:46.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:39:47.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -91 - }, - { - "mac": 4880535074783, - "rawRssi": -67 - }, - { - "mac": 176353769414806, - "rawRssi": -76 - }, - { - "mac": 4880535074782, - "rawRssi": -59 - }, - { - "mac": 123486069126043, - "rawRssi": -73 - }, - { - "mac": 13685728359546, - "rawRssi": -90 - }, - { - "mac": 13685719791891, - "rawRssi": -80 - }, - { - "mac": 233254798443903, - "rawRssi": -80 - }, - { - "mac": 176156956989012, - "rawRssi": -58 - }, - { - "mac": 178355980244552, - "rawRssi": -25 - }, - { - "mac": 176156956989009, - "rawRssi": -63 - }, - { - "mac": 5175406356059, - "rawRssi": -72 - }, - { - "mac": 233833088632553, - "rawRssi": -82 - }, - { - "mac": 13685719791895, - "rawRssi": -82 - }, - { - "mac": 5175406356058, - "rawRssi": -82 - }, - { - "mac": 79565174465541, - "rawRssi": -89 - }, - { - "mac": 207775459763230, - "rawRssi": -61 - }, - { - "mac": 207775459763231, - "rawRssi": -71 - }, - { - "mac": 272875381228877, - "rawRssi": -84 - }, - { - "mac": 251751037760296, - "rawRssi": -69 - }, - { - "mac": 176353769414803, - "rawRssi": -73 - }, - { - "mac": 253950061015863, - "rawRssi": -59 - }, - { - "mac": 176156956988992, - "rawRssi": -47 - }, - { - "mac": 66207235812212, - "rawRssi": -90 - }, - { - "mac": 176156956988996, - "rawRssi": -37 - }, - { - "mac": 245000064254810, - "rawRssi": -71 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:39:51.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:39:56.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:40:01.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632513°, 10.7011246°", - "accuracyMeters": 3, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:40:04.000+02:00", - "speedMetersPerSecond": 0.0010243849828839302 - } - }, - { - "position": { - "LatLng": "53.863244°, 10.701143°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:40:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T09:41:10.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:41:11.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -90 - }, - { - "mac": 81764197721092, - "rawRssi": -75 - }, - { - "mac": 4880535074783, - "rawRssi": -69 - }, - { - "mac": 248525417280042, - "rawRssi": -91 - }, - { - "mac": 176353769414806, - "rawRssi": -72 - }, - { - "mac": 4880535074782, - "rawRssi": -57 - }, - { - "mac": 79565160561131, - "rawRssi": -91 - }, - { - "mac": 118796079720199, - "rawRssi": -80 - }, - { - "mac": 13685728359546, - "rawRssi": -86 - }, - { - "mac": 13685719791891, - "rawRssi": -78 - }, - { - "mac": 136877347993184, - "rawRssi": -85 - }, - { - "mac": 13685723516731, - "rawRssi": -83 - }, - { - "mac": 176156956989012, - "rawRssi": -54 - }, - { - "mac": 4880535074786, - "rawRssi": -69 - }, - { - "mac": 176156956989009, - "rawRssi": -52 - }, - { - "mac": 5175406356059, - "rawRssi": -69 - }, - { - "mac": 233833088632553, - "rawRssi": -79 - }, - { - "mac": 79965018526574, - "rawRssi": -83 - }, - { - "mac": 233833088632552, - "rawRssi": -65 - }, - { - "mac": 13685719791895, - "rawRssi": -89 - }, - { - "mac": 5175406356058, - "rawRssi": -71 - }, - { - "mac": 88949690804935, - "rawRssi": -82 - }, - { - "mac": 246326394024490, - "rawRssi": -92 - }, - { - "mac": 88949690804936, - "rawRssi": -90 - }, - { - "mac": 79565174465540, - "rawRssi": -75 - }, - { - "mac": 79565174465541, - "rawRssi": -90 - }, - { - "mac": 207775459763230, - "rawRssi": -67 - }, - { - "mac": 207775459763231, - "rawRssi": -70 - }, - { - "mac": 272875381228877, - "rawRssi": -80 - }, - { - "mac": 251751037760296, - "rawRssi": -64 - }, - { - "mac": 251751037760295, - "rawRssi": -61 - }, - { - "mac": 253950061015831, - "rawRssi": -60 - }, - { - "mac": 176353769414803, - "rawRssi": -69 - }, - { - "mac": 13685728359545, - "rawRssi": -80 - }, - { - "mac": 253950061015863, - "rawRssi": -60 - }, - { - "mac": 176156956988992, - "rawRssi": -40 - }, - { - "mac": 162987945711694, - "rawRssi": -81 - }, - { - "mac": 66207235812212, - "rawRssi": -87 - }, - { - "mac": 176156956988996, - "rawRssi": -30 - }, - { - "mac": 245000064254810, - "rawRssi": -72 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632529°, 10.7011266°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:41:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:41:45.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632539°, 10.7011255°", - "accuracyMeters": 17, - "altitudeMeters": 53.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:42:07.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:42:20.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863255°, 10.7011252°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:42:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632532°, 10.7011247°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:43:17.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:43:30.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632535°, 10.7011265°", - "accuracyMeters": 31, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:43:52.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632521°, 10.7011272°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:44:26.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:44:39.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632527°, 10.701129°", - "accuracyMeters": 28, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:45:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632521°, 10.7011316°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:45:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:45:48.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632507°, 10.701133°", - "accuracyMeters": 15, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:46:09.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632504°, 10.7011329°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:46:43.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:46:56.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632512°, 10.7011354°", - "accuracyMeters": 21, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:47:17.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632503°, 10.7011345°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:47:52.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:48:05.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863251°, 10.7011349°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:48:25.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632516°, 10.7011351°", - "accuracyMeters": 18, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:48:59.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:49:12.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632527°, 10.7011361°", - "accuracyMeters": 23, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:49:34.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632539°, 10.701139°", - "accuracyMeters": 20, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:50:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:50:21.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632541°, 10.701139°", - "accuracyMeters": 23, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:50:43.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632544°, 10.7011393°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:51:19.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632545°, 10.7011394°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:51:56.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632559°, 10.7011396°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:52:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632562°, 10.7011399°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:53:09.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632562°, 10.7011387°", - "accuracyMeters": 19, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:53:45.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:53:51.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632568°, 10.701139°", - "accuracyMeters": 20, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:54:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863257°, 10.7011391°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:54:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632574°, 10.7011386°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:55:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632573°, 10.7011383°", - "accuracyMeters": 19, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:56:11.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632575°, 10.7011383°", - "accuracyMeters": 19, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:56:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.8600000143051147 - }, - { - "type": "WALKING", - "confidence": 0.8600000143051147 - }, - { - "type": "STILL", - "confidence": 0.05999999865889549 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.029999999329447746 - }, - { - "type": "UNKNOWN", - "confidence": 0.029999999329447746 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "RUNNING", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T09:57:09.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633114°, 10.7010428°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:57:19.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T09:57:29.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721092, - "rawRssi": -82 - }, - { - "mac": 4880535074783, - "rawRssi": -69 - }, - { - "mac": 176353769414806, - "rawRssi": -76 - }, - { - "mac": 4880535074782, - "rawRssi": -67 - }, - { - "mac": 162987945711698, - "rawRssi": -92 - }, - { - "mac": 123486069126043, - "rawRssi": -76 - }, - { - "mac": 118796079720199, - "rawRssi": -85 - }, - { - "mac": 198996529455448, - "rawRssi": -77 - }, - { - "mac": 13685719791891, - "rawRssi": -88 - }, - { - "mac": 198996529455449, - "rawRssi": -83 - }, - { - "mac": 136877347993184, - "rawRssi": -85 - }, - { - "mac": 233254798443903, - "rawRssi": -83 - }, - { - "mac": 176156956989012, - "rawRssi": -54 - }, - { - "mac": 176156956989009, - "rawRssi": -57 - }, - { - "mac": 4880535074786, - "rawRssi": -70 - }, - { - "mac": 233833088632553, - "rawRssi": -92 - }, - { - "mac": 233833088632552, - "rawRssi": -79 - }, - { - "mac": 13685719791895, - "rawRssi": -92 - }, - { - "mac": 5175406356058, - "rawRssi": -92 - }, - { - "mac": 196031866815400, - "rawRssi": -73 - }, - { - "mac": 66692969804511, - "rawRssi": -86 - }, - { - "mac": 103893075959290, - "rawRssi": -85 - }, - { - "mac": 103893075959292, - "rawRssi": -85 - }, - { - "mac": 233673614513711, - "rawRssi": -93 - }, - { - "mac": 233673614513710, - "rawRssi": -85 - }, - { - "mac": 207775459763230, - "rawRssi": -73 - }, - { - "mac": 236032111888024, - "rawRssi": -66 - }, - { - "mac": 207775459763231, - "rawRssi": -75 - }, - { - "mac": 187152073266777, - "rawRssi": -54 - }, - { - "mac": 251751037760296, - "rawRssi": -80 - }, - { - "mac": 251751037760295, - "rawRssi": -78 - }, - { - "mac": 253950061015831, - "rawRssi": -73 - }, - { - "mac": 162987945711695, - "rawRssi": -94 - }, - { - "mac": 176353769414803, - "rawRssi": -74 - }, - { - "mac": 253950061015863, - "rawRssi": -85 - }, - { - "mac": 176156956988992, - "rawRssi": -79 - }, - { - "mac": 162987945711694, - "rawRssi": -84 - }, - { - "mac": 176156956988996, - "rawRssi": -70 - }, - { - "mac": 245000064254810, - "rawRssi": -65 - } - ] - } - }, - { - "position": { - "LatLng": "53.8633178°, 10.7011127°", - "accuracyMeters": 10, - "altitudeMeters": 50.175209045410156, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:58:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T09:58:22.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633118°, 10.7010656°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:58:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632727°, 10.7011825°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:59:18.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9300000071525574 - }, - { - "type": "WALKING", - "confidence": 0.9300000071525574 - }, - { - "type": "STILL", - "confidence": 0.029999999329447746 - }, - { - "type": "RUNNING", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.019999999552965164 - } - ], - "timestamp": "2025-07-04T09:59:18.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632785°, 10.7012188°", - "accuracyMeters": 38, - "altitudeMeters": 54.5, - "source": "UNKNOWN", - "timestamp": "2025-07-04T09:59:50.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T09:59:52.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8634285°, 10.7024834°", - "accuracyMeters": 15, - "altitudeMeters": 47.89999771118164, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:00:25.000+02:00", - "speedMetersPerSecond": 1.1378525495529175 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9300000071525574 - }, - { - "type": "WALKING", - "confidence": 0.9300000071525574 - }, - { - "type": "RUNNING", - "confidence": 0.019999999552965164 - }, - { - "type": "STILL", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.019999999552965164 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:00:26.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:00:51.000+02:00", - "devicesRecords": [ - { - "mac": 105640739260141, - "rawRssi": -82 - }, - { - "mac": 26948982803099, - "rawRssi": -93 - }, - { - "mac": 253393877023191, - "rawRssi": -92 - }, - { - "mac": 23046690902117, - "rawRssi": -85 - }, - { - "mac": 110663110646100, - "rawRssi": -84 - }, - { - "mac": 194553567586464, - "rawRssi": -91 - }, - { - "mac": 194522227760685, - "rawRssi": -90 - }, - { - "mac": 251194867268254, - "rawRssi": -82 - }, - { - "mac": 177018967513232, - "rawRssi": -77 - }, - { - "mac": 29148006058648, - "rawRssi": -94 - }, - { - "mac": 5175411612109, - "rawRssi": -91 - }, - { - "mac": 79565170129401, - "rawRssi": -81 - }, - { - "mac": 80135124857877, - "rawRssi": -82 - }, - { - "mac": 179217990768788, - "rawRssi": -77 - }, - { - "mac": 179217990768786, - "rawRssi": -78 - }, - { - "mac": 31806105158604, - "rawRssi": -81 - }, - { - "mac": 88673306087965, - "rawRssi": -91 - }, - { - "mac": 88673306087964, - "rawRssi": -85 - }, - { - "mac": 88949687034511, - "rawRssi": -87 - }, - { - "mac": 139634985482520, - "rawRssi": -89 - }, - { - "mac": 158725965403596, - "rawRssi": -74 - }, - { - "mac": 13745569760473, - "rawRssi": -83 - }, - { - "mac": 275495485798314, - "rawRssi": -70 - }, - { - "mac": 79565158507849, - "rawRssi": -78 - }, - { - "mac": 241986119955079, - "rawRssi": -81 - }, - { - "mac": 13745569760474, - "rawRssi": -87 - }, - { - "mac": 233254802604954, - "rawRssi": -87 - }, - { - "mac": 241986119955078, - "rawRssi": -85 - }, - { - "mac": 102229435112958, - "rawRssi": -86 - }, - { - "mac": 90872329343548, - "rawRssi": -85 - }, - { - "mac": 141254785589296, - "rawRssi": -91 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T10:01:00.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632022°, 10.7022137°", - "accuracyMeters": 13, - "altitudeMeters": 49.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:01:17.000+02:00", - "speedMetersPerSecond": 1.090000033378601 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:01:25.000+02:00", - "devicesRecords": [ - { - "mac": 154042044640441, - "rawRssi": -84 - }, - { - "mac": 31806105158605, - "rawRssi": -85 - }, - { - "mac": 31806105158604, - "rawRssi": -89 - }, - { - "mac": 139634985482521, - "rawRssi": -95 - }, - { - "mac": 139634985482520, - "rawRssi": -85 - }, - { - "mac": 79565158507850, - "rawRssi": -91 - }, - { - "mac": 154042044640444, - "rawRssi": -93 - }, - { - "mac": 154042044640445, - "rawRssi": -92 - }, - { - "mac": 251194867268254, - "rawRssi": -86 - }, - { - "mac": 49004160307828, - "rawRssi": -87 - }, - { - "mac": 79565158507849, - "rawRssi": -84 - }, - { - "mac": 241986119955078, - "rawRssi": -92 - }, - { - "mac": 66207237488250, - "rawRssi": -88 - }, - { - "mac": 90872329343516, - "rawRssi": -85 - }, - { - "mac": 110827922517988, - "rawRssi": -85 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9599999785423279 - }, - { - "type": "WALKING", - "confidence": 0.9599999785423279 - }, - { - "type": "STILL", - "confidence": 0.029999999329447746 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:01:33.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632078°, 10.7016663°", - "accuracyMeters": 17, - "altitudeMeters": 56.04386520385742, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:01:49.000+02:00", - "speedMetersPerSecond": 0.8399999737739563 - } - }, - { - "position": { - "LatLng": "53.8631964°, 10.701264°", - "accuracyMeters": 7, - "altitudeMeters": 56.04386520385742, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:02:24.000+02:00", - "speedMetersPerSecond": 1.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.8899999856948853 - }, - { - "type": "WALKING", - "confidence": 0.8899999856948853 - }, - { - "type": "STILL", - "confidence": 0.05000000074505806 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.029999999329447746 - }, - { - "type": "RUNNING", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:02:41.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633038°, 10.7010624°", - "accuracyMeters": 12, - "altitudeMeters": 58.12854766845703, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:03:02.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T10:03:13.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:03:19.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721092, - "rawRssi": -85 - }, - { - "mac": 4880535074783, - "rawRssi": -71 - }, - { - "mac": 176353769414806, - "rawRssi": -79 - }, - { - "mac": 4880535074782, - "rawRssi": -59 - }, - { - "mac": 123486069126043, - "rawRssi": -81 - }, - { - "mac": 198996529455448, - "rawRssi": -79 - }, - { - "mac": 198996529455449, - "rawRssi": -90 - }, - { - "mac": 233254798443902, - "rawRssi": -86 - }, - { - "mac": 233254798443903, - "rawRssi": -79 - }, - { - "mac": 176156956989012, - "rawRssi": -56 - }, - { - "mac": 4880535074786, - "rawRssi": -70 - }, - { - "mac": 176156956989009, - "rawRssi": -61 - }, - { - "mac": 236032111888056, - "rawRssi": -78 - }, - { - "mac": 233833088632552, - "rawRssi": -79 - }, - { - "mac": 196031866815400, - "rawRssi": -64 - }, - { - "mac": 66692969804511, - "rawRssi": -88 - }, - { - "mac": 79565174465540, - "rawRssi": -85 - }, - { - "mac": 233673614513710, - "rawRssi": -86 - }, - { - "mac": 207775459763230, - "rawRssi": -73 - }, - { - "mac": 207775459763231, - "rawRssi": -72 - }, - { - "mac": 236032111888024, - "rawRssi": -74 - }, - { - "mac": 251751037760296, - "rawRssi": -84 - }, - { - "mac": 241986117947284, - "rawRssi": -86 - }, - { - "mac": 253950061015831, - "rawRssi": -82 - }, - { - "mac": 176353769414803, - "rawRssi": -76 - }, - { - "mac": 253950061015863, - "rawRssi": -83 - }, - { - "mac": 176156956988992, - "rawRssi": -74 - }, - { - "mac": 162987945711694, - "rawRssi": -84 - }, - { - "mac": 176156956988996, - "rawRssi": -60 - }, - { - "mac": 245000064254810, - "rawRssi": -81 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T10:03:45.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632772°, 10.7011043°", - "accuracyMeters": 11, - "altitudeMeters": 56.04386520385742, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:03:45.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632755°, 10.7010911°", - "accuracyMeters": 11, - "altitudeMeters": 56.04386520385742, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:04:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:04:51.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632698°, 10.7010983°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:05:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:05:13.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632798°, 10.7011074°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:05:56.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:06:17.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632745°, 10.7011001°", - "accuracyMeters": 11, - "altitudeMeters": 55.357513427734375, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:06:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:07:22.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632771°, 10.701083°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:07:22.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632839°, 10.7010937°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:08:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:08:06.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632799°, 10.7011024°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:08:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:09:08.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632796°, 10.7011042°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:09:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:10:12.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632869°, 10.7010922°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:10:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632814°, 10.7010983°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:10:55.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:11:17.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632816°, 10.7011032°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:11:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:12:23.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632888°, 10.7010881°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:12:23.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632826°, 10.7010972°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:13:06.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T10:13:28.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:13:36.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -92 - }, - { - "mac": 4880535074783, - "rawRssi": -71 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -60 - }, - { - "mac": 123486069126043, - "rawRssi": -84 - }, - { - "mac": 203326496528334, - "rawRssi": -84 - }, - { - "mac": 13685719791891, - "rawRssi": -74 - }, - { - "mac": 13685718670642, - "rawRssi": -84 - }, - { - "mac": 13685723516731, - "rawRssi": -87 - }, - { - "mac": 233254798443903, - "rawRssi": -82 - }, - { - "mac": 176156956989012, - "rawRssi": -60 - }, - { - "mac": 4880535074786, - "rawRssi": -71 - }, - { - "mac": 176156956989009, - "rawRssi": -61 - }, - { - "mac": 236032111888056, - "rawRssi": -66 - }, - { - "mac": 5175406356059, - "rawRssi": -79 - }, - { - "mac": 233833088632553, - "rawRssi": -86 - }, - { - "mac": 233833088632552, - "rawRssi": -64 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -83 - }, - { - "mac": 196031866815400, - "rawRssi": -67 - }, - { - "mac": 66692969804511, - "rawRssi": -85 - }, - { - "mac": 15884746772283, - "rawRssi": -87 - }, - { - "mac": 79565174465541, - "rawRssi": -91 - }, - { - "mac": 207775459763230, - "rawRssi": -60 - }, - { - "mac": 272875381228876, - "rawRssi": -79 - }, - { - "mac": 236032111888024, - "rawRssi": -67 - }, - { - "mac": 207775459763231, - "rawRssi": -75 - }, - { - "mac": 272875381228877, - "rawRssi": -89 - }, - { - "mac": 251751037760296, - "rawRssi": -76 - }, - { - "mac": 253950061015831, - "rawRssi": -66 - }, - { - "mac": 176353769414803, - "rawRssi": -74 - }, - { - "mac": 253950061015863, - "rawRssi": -68 - }, - { - "mac": 176156956988992, - "rawRssi": -36 - }, - { - "mac": 162987945711694, - "rawRssi": -76 - }, - { - "mac": 176156956988996, - "rawRssi": -28 - }, - { - "mac": 245000064254810, - "rawRssi": -75 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632869°, 10.7011092°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:14:11.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7010853°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:14:55.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863291°, 10.7010883°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:15:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632849°, 10.7011058°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:16:22.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632922°, 10.7011014°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:17:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.86328°, 10.7011187°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:17:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632813°, 10.7011053°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:18:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7010979°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:19:16.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632931°, 10.7010962°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:19:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632842°, 10.7010954°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:20:41.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863288°, 10.7010795°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:21:25.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863288°, 10.7011052°", - "accuracyMeters": 35, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:22:04.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632862°, 10.7010847°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:22:53.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863279°, 10.7010943°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:23:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.86328°, 10.7010932°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:24:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632954°, 10.7010959°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:25:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632906°, 10.7010902°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:25:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632919°, 10.7010945°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:26:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632868°, 10.7011029°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:27:16.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632863°, 10.7010762°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:27:55.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:27:56.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -71 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -62 - }, - { - "mac": 123486069126043, - "rawRssi": -81 - }, - { - "mac": 203326496528334, - "rawRssi": -85 - }, - { - "mac": 13685719791891, - "rawRssi": -71 - }, - { - "mac": 233254798443902, - "rawRssi": -92 - }, - { - "mac": 176156956989012, - "rawRssi": -62 - }, - { - "mac": 176156956989009, - "rawRssi": -64 - }, - { - "mac": 236032111888056, - "rawRssi": -72 - }, - { - "mac": 5175406356059, - "rawRssi": -80 - }, - { - "mac": 233833088632553, - "rawRssi": -85 - }, - { - "mac": 233833088632552, - "rawRssi": -75 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 196031866815400, - "rawRssi": -70 - }, - { - "mac": 207775459763230, - "rawRssi": -66 - }, - { - "mac": 236032111888024, - "rawRssi": -68 - }, - { - "mac": 207775459763231, - "rawRssi": -73 - }, - { - "mac": 272875381228877, - "rawRssi": -82 - }, - { - "mac": 251751037760296, - "rawRssi": -75 - }, - { - "mac": 251751037760295, - "rawRssi": -67 - }, - { - "mac": 253950061015831, - "rawRssi": -68 - }, - { - "mac": 176353769414803, - "rawRssi": -75 - }, - { - "mac": 253950061015863, - "rawRssi": -79 - }, - { - "mac": 176156956988992, - "rawRssi": -44 - }, - { - "mac": 162987945711694, - "rawRssi": -78 - }, - { - "mac": 176156956988996, - "rawRssi": -35 - }, - { - "mac": 245000064254810, - "rawRssi": -73 - } - ] - } - }, - { - "position": { - "LatLng": "53.8634499°, 10.701068°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:28:41.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632878°, 10.701078°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:29:15.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633181°, 10.7010789°", - "accuracyMeters": 18, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:29:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632919°, 10.7010836°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:30:36.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633044°, 10.7010931°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:31:17.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863299°, 10.7010948°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:31:53.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632963°, 10.7011031°", - "accuracyMeters": 30, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:32:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632941°, 10.7010965°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:33:04.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.86329°, 10.7011051°", - "accuracyMeters": 19, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:33:45.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632891°, 10.7011059°", - "accuracyMeters": 15, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:34:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632888°, 10.7011035°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:34:56.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632887°, 10.7011051°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:35:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632917°, 10.7010849°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:36:07.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632837°, 10.7011037°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:36:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632833°, 10.701099°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:37:25.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633041°, 10.7010855°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:38:02.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632832°, 10.7011054°", - "accuracyMeters": 18, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:38:43.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:39:00.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -92 - }, - { - "mac": 81764197721092, - "rawRssi": -83 - }, - { - "mac": 4880535074783, - "rawRssi": -74 - }, - { - "mac": 176353769414806, - "rawRssi": -80 - }, - { - "mac": 4880535074782, - "rawRssi": -60 - }, - { - "mac": 207775459763234, - "rawRssi": -68 - }, - { - "mac": 123486069126043, - "rawRssi": -75 - }, - { - "mac": 203326496528332, - "rawRssi": -92 - }, - { - "mac": 198996529455448, - "rawRssi": -82 - }, - { - "mac": 13685719791891, - "rawRssi": -76 - }, - { - "mac": 233254798443903, - "rawRssi": -77 - }, - { - "mac": 176156956989012, - "rawRssi": -51 - }, - { - "mac": 178355980244552, - "rawRssi": -31 - }, - { - "mac": 101694052703736, - "rawRssi": -84 - }, - { - "mac": 176156956989009, - "rawRssi": -56 - }, - { - "mac": 236032111888056, - "rawRssi": -63 - }, - { - "mac": 233833088632553, - "rawRssi": -80 - }, - { - "mac": 233833088632552, - "rawRssi": -61 - }, - { - "mac": 13685719791895, - "rawRssi": -83 - }, - { - "mac": 88949690804935, - "rawRssi": -83 - }, - { - "mac": 196031866815400, - "rawRssi": -66 - }, - { - "mac": 66692969804511, - "rawRssi": -82 - }, - { - "mac": 103893075959290, - "rawRssi": -85 - }, - { - "mac": 103893075959292, - "rawRssi": -85 - }, - { - "mac": 101694052703744, - "rawRssi": -91 - }, - { - "mac": 79565174465541, - "rawRssi": -91 - }, - { - "mac": 207775459763230, - "rawRssi": -63 - }, - { - "mac": 272875381228876, - "rawRssi": -70 - }, - { - "mac": 236032111888024, - "rawRssi": -65 - }, - { - "mac": 207775459763231, - "rawRssi": -68 - }, - { - "mac": 272875381228877, - "rawRssi": -81 - }, - { - "mac": 187152073266777, - "rawRssi": -51 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -65 - }, - { - "mac": 253950061015831, - "rawRssi": -64 - }, - { - "mac": 176353769414803, - "rawRssi": -66 - }, - { - "mac": 13685728359545, - "rawRssi": -78 - }, - { - "mac": 176156956988992, - "rawRssi": -37 - }, - { - "mac": 162987945711694, - "rawRssi": -74 - }, - { - "mac": 176156956988996, - "rawRssi": -31 - }, - { - "mac": 245000064254810, - "rawRssi": -66 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632815°, 10.7011066°", - "accuracyMeters": 34, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:39:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863294°, 10.7010681°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:40:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632851°, 10.7010808°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:40:52.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632822°, 10.7011084°", - "accuracyMeters": 23, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:41:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632844°, 10.7011046°", - "accuracyMeters": 35, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:42:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632939°, 10.7010624°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:42:50.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632927°, 10.7010807°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:43:27.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632956°, 10.7010904°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:44:04.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632951°, 10.701082°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:44:46.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632809°, 10.7011113°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:45:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632899°, 10.7010958°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:46:07.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632943°, 10.7010969°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:46:51.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632809°, 10.7011111°", - "accuracyMeters": 22, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:47:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632898°, 10.7010647°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:48:10.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632811°, 10.7011106°", - "accuracyMeters": 22, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:48:53.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:49:04.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -91 - }, - { - "mac": 4880535074783, - "rawRssi": -73 - }, - { - "mac": 176353769414806, - "rawRssi": -77 - }, - { - "mac": 4880535074782, - "rawRssi": -53 - }, - { - "mac": 207775459763234, - "rawRssi": -67 - }, - { - "mac": 123486069126043, - "rawRssi": -70 - }, - { - "mac": 198996529455448, - "rawRssi": -86 - }, - { - "mac": 13685719791891, - "rawRssi": -72 - }, - { - "mac": 13685718670642, - "rawRssi": -80 - }, - { - "mac": 233254798443903, - "rawRssi": -76 - }, - { - "mac": 176156956989012, - "rawRssi": -53 - }, - { - "mac": 178355980244552, - "rawRssi": -28 - }, - { - "mac": 176156956989009, - "rawRssi": -51 - }, - { - "mac": 236032111888056, - "rawRssi": -64 - }, - { - "mac": 5175406356059, - "rawRssi": -77 - }, - { - "mac": 233833088632553, - "rawRssi": -80 - }, - { - "mac": 233833088632552, - "rawRssi": -58 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -77 - }, - { - "mac": 88949690804935, - "rawRssi": -80 - }, - { - "mac": 66692969804511, - "rawRssi": -85 - }, - { - "mac": 79565174465540, - "rawRssi": -78 - }, - { - "mac": 79565174465541, - "rawRssi": -92 - }, - { - "mac": 207775459763230, - "rawRssi": -60 - }, - { - "mac": 236032111888024, - "rawRssi": -58 - }, - { - "mac": 207775459763231, - "rawRssi": -67 - }, - { - "mac": 272875381228877, - "rawRssi": -82 - }, - { - "mac": 187152073266777, - "rawRssi": -54 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -66 - }, - { - "mac": 176353769414803, - "rawRssi": -65 - }, - { - "mac": 13685728359545, - "rawRssi": -80 - }, - { - "mac": 176156956988992, - "rawRssi": -31 - }, - { - "mac": 162987945711694, - "rawRssi": -79 - }, - { - "mac": 176156956988996, - "rawRssi": -31 - }, - { - "mac": 245000064254810, - "rawRssi": -62 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632812°, 10.7011101°", - "accuracyMeters": 26, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:49:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863291°, 10.7010725°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:50:06.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632984°, 10.7010863°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:50:50.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632802°, 10.7011101°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:51:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632798°, 10.7011102°", - "accuracyMeters": 22, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:52:09.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632798°, 10.7011103°", - "accuracyMeters": 24, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:52:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632924°, 10.701069°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:53:24.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632807°, 10.7011103°", - "accuracyMeters": 36, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:54:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632985°, 10.7010768°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:54:44.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863282°, 10.7011097°", - "accuracyMeters": 21, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:55:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632902°, 10.7010761°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:56:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632825°, 10.7011093°", - "accuracyMeters": 22, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:56:49.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632825°, 10.7011092°", - "accuracyMeters": 25, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:57:28.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632825°, 10.7011087°", - "accuracyMeters": 31, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:58:10.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632822°, 10.7011087°", - "accuracyMeters": 19, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:58:46.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T10:59:07.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -70 - }, - { - "mac": 176353769414806, - "rawRssi": -81 - }, - { - "mac": 4880535074782, - "rawRssi": -65 - }, - { - "mac": 123486069126043, - "rawRssi": -82 - }, - { - "mac": 176156956989012, - "rawRssi": -59 - }, - { - "mac": 176156956989009, - "rawRssi": -65 - }, - { - "mac": 236032111888056, - "rawRssi": -72 - }, - { - "mac": 5175406356059, - "rawRssi": -74 - }, - { - "mac": 233833088632553, - "rawRssi": -88 - }, - { - "mac": 233833088632552, - "rawRssi": -72 - }, - { - "mac": 5175406356058, - "rawRssi": -85 - }, - { - "mac": 196031866815400, - "rawRssi": -70 - }, - { - "mac": 207775459763230, - "rawRssi": -69 - }, - { - "mac": 272875381228876, - "rawRssi": -69 - }, - { - "mac": 236032111888024, - "rawRssi": -68 - }, - { - "mac": 207775459763231, - "rawRssi": -71 - }, - { - "mac": 272875381228877, - "rawRssi": -81 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -65 - }, - { - "mac": 253950061015831, - "rawRssi": -67 - }, - { - "mac": 176353769414803, - "rawRssi": -78 - }, - { - "mac": 110827922517988, - "rawRssi": -85 - }, - { - "mac": 253950061015863, - "rawRssi": -65 - }, - { - "mac": 225185649798751, - "rawRssi": -81 - }, - { - "mac": 176156956988992, - "rawRssi": -51 - }, - { - "mac": 162987945711694, - "rawRssi": -79 - }, - { - "mac": 66207235812212, - "rawRssi": -92 - }, - { - "mac": 176156956988996, - "rawRssi": -36 - }, - { - "mac": 245000064254810, - "rawRssi": -69 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632824°, 10.7011086°", - "accuracyMeters": 37, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T10:59:41.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632785°, 10.701101°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:00:18.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632818°, 10.7011085°", - "accuracyMeters": 37, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:01:01.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632884°, 10.7010965°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:01:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632887°, 10.7010988°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:02:16.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632818°, 10.7011084°", - "accuracyMeters": 37, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:03:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632855°, 10.7011088°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:03:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863282°, 10.7011085°", - "accuracyMeters": 48, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:04:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632825°, 10.7011087°", - "accuracyMeters": 31, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:05:03.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632826°, 10.7011088°", - "accuracyMeters": 38, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:05:46.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.86329°, 10.7011039°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:06:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863282°, 10.701109°", - "accuracyMeters": 30, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:07:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632862°, 10.7010954°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:07:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632828°, 10.7011096°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:08:26.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632835°, 10.7011103°", - "accuracyMeters": 9, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:09:01.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T11:09:17.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632835°, 10.7011109°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:09:36.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632836°, 10.7011111°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:10:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632835°, 10.7011111°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:10:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632835°, 10.7011109°", - "accuracyMeters": 9, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:11:23.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632845°, 10.7011109°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:11:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632855°, 10.7011116°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:12:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632851°, 10.7011117°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:13:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632854°, 10.7011117°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:13:44.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632855°, 10.7011117°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:14:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011119°", - "accuracyMeters": 18, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:14:57.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632853°, 10.701112°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:15:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632857°, 10.701112°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:16:09.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7011121°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:16:45.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T11:17:02.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -77 - }, - { - "mac": 176353769414806, - "rawRssi": -79 - }, - { - "mac": 4880535074782, - "rawRssi": -60 - }, - { - "mac": 123486069126043, - "rawRssi": -76 - }, - { - "mac": 13685719791891, - "rawRssi": -72 - }, - { - "mac": 176156956989012, - "rawRssi": -59 - }, - { - "mac": 178355980244552, - "rawRssi": -37 - }, - { - "mac": 176156956989009, - "rawRssi": -63 - }, - { - "mac": 5175406356059, - "rawRssi": -66 - }, - { - "mac": 233833088632553, - "rawRssi": -79 - }, - { - "mac": 13685719791895, - "rawRssi": -82 - }, - { - "mac": 66692969804511, - "rawRssi": -84 - }, - { - "mac": 79565174465540, - "rawRssi": -79 - }, - { - "mac": 207775459763230, - "rawRssi": -70 - }, - { - "mac": 236032111888024, - "rawRssi": -67 - }, - { - "mac": 207775459763231, - "rawRssi": -74 - }, - { - "mac": 272875381228877, - "rawRssi": -81 - }, - { - "mac": 251751037760296, - "rawRssi": -68 - }, - { - "mac": 251751037760295, - "rawRssi": -60 - }, - { - "mac": 253950061015831, - "rawRssi": -58 - }, - { - "mac": 176353769414803, - "rawRssi": -73 - }, - { - "mac": 13685728359545, - "rawRssi": -75 - }, - { - "mac": 253950061015863, - "rawRssi": -60 - }, - { - "mac": 176156956988992, - "rawRssi": -47 - }, - { - "mac": 162987945711694, - "rawRssi": -79 - }, - { - "mac": 176156956988996, - "rawRssi": -38 - }, - { - "mac": 245000064254810, - "rawRssi": -64 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632866°, 10.7011122°", - "accuracyMeters": 21, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:17:22.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632869°, 10.7011124°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:17:59.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632865°, 10.7011129°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:18:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632863°, 10.701113°", - "accuracyMeters": 20, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:19:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011128°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:19:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863285°, 10.7011131°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:20:24.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632848°, 10.7011131°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:21:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863285°, 10.7011132°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:21:36.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632855°, 10.7011136°", - "accuracyMeters": 8, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:22:13.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632857°, 10.7011137°", - "accuracyMeters": 21, - "altitudeMeters": 53.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:22:49.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632864°, 10.7011138°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:23:25.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632869°, 10.7011139°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:24:02.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632868°, 10.7011138°", - "accuracyMeters": 20, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:24:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863287°, 10.7011139°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:25:16.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632871°, 10.7011138°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:25:54.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632869°, 10.7011138°", - "accuracyMeters": 18, - "altitudeMeters": 53.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:26:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632867°, 10.7011138°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:27:06.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T11:27:38.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721092, - "rawRssi": -79 - }, - { - "mac": 4880535074783, - "rawRssi": -77 - }, - { - "mac": 176353769414806, - "rawRssi": -79 - }, - { - "mac": 4880535074782, - "rawRssi": -64 - }, - { - "mac": 123486069126043, - "rawRssi": -75 - }, - { - "mac": 203326496528334, - "rawRssi": -80 - }, - { - "mac": 13685719791891, - "rawRssi": -70 - }, - { - "mac": 13685718670642, - "rawRssi": -79 - }, - { - "mac": 13685723516731, - "rawRssi": -88 - }, - { - "mac": 176156956989012, - "rawRssi": -59 - }, - { - "mac": 176156956989009, - "rawRssi": -61 - }, - { - "mac": 236032111888056, - "rawRssi": -63 - }, - { - "mac": 5175406356059, - "rawRssi": -65 - }, - { - "mac": 233833088632553, - "rawRssi": -81 - }, - { - "mac": 233833088632552, - "rawRssi": -61 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -84 - }, - { - "mac": 196031866815400, - "rawRssi": -63 - }, - { - "mac": 66692969804511, - "rawRssi": -84 - }, - { - "mac": 103893075959290, - "rawRssi": -85 - }, - { - "mac": 15884746772283, - "rawRssi": -87 - }, - { - "mac": 79565174465540, - "rawRssi": -79 - }, - { - "mac": 207775459763230, - "rawRssi": -72 - }, - { - "mac": 236032111888024, - "rawRssi": -60 - }, - { - "mac": 207775459763231, - "rawRssi": -75 - }, - { - "mac": 187152073266777, - "rawRssi": -59 - }, - { - "mac": 251751037760296, - "rawRssi": -68 - }, - { - "mac": 251751037760295, - "rawRssi": -66 - }, - { - "mac": 253950061015831, - "rawRssi": -64 - }, - { - "mac": 176353769414803, - "rawRssi": -66 - }, - { - "mac": 253950061015863, - "rawRssi": -61 - }, - { - "mac": 176156956988992, - "rawRssi": -50 - }, - { - "mac": 176156956988996, - "rawRssi": -38 - }, - { - "mac": 245000064254810, - "rawRssi": -70 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632865°, 10.7011137°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:27:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632866°, 10.7011137°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:28:17.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632661°, 10.7011131°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:28:55.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632509°, 10.7011195°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:29:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632643°, 10.7011172°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:30:09.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632733°, 10.7011332°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:30:44.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632581°, 10.7011095°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:31:20.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632834°, 10.7011216°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:31:56.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632806°, 10.7010969°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:32:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632746°, 10.7011046°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:33:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632788°, 10.7011097°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:33:43.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632773°, 10.7011121°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:34:18.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632673°, 10.7011161°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:34:53.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632597°, 10.7011135°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:35:28.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632618°, 10.7010926°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:36:03.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632793°, 10.7010949°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:36:38.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632769°, 10.7011265°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:37:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632689°, 10.7011165°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:37:49.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632915°, 10.7010772°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:38:24.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632865°, 10.7011323°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:39:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011261°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:39:38.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632494°, 10.7010957°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:40:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632661°, 10.7010884°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:40:48.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632755°, 10.7011028°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:41:24.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011158°", - "accuracyMeters": 8, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:42:04.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7011159°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:42:41.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632583°, 10.7011085°", - "accuracyMeters": 38, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:43:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863283°, 10.7011494°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:43:55.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632753°, 10.701107°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:44:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632556°, 10.7010789°", - "accuracyMeters": 14, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:45:10.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T11:45:28.000+02:00", - "devicesRecords": [ - { - "mac": 158820375294976, - "rawRssi": -80 - }, - { - "mac": 4880535074783, - "rawRssi": -77 - }, - { - "mac": 176353769414806, - "rawRssi": -82 - }, - { - "mac": 4880535074782, - "rawRssi": -67 - }, - { - "mac": 123486069126043, - "rawRssi": -77 - }, - { - "mac": 203326496528334, - "rawRssi": -77 - }, - { - "mac": 13685728359546, - "rawRssi": -87 - }, - { - "mac": 13685719791891, - "rawRssi": -66 - }, - { - "mac": 176156956989012, - "rawRssi": -52 - }, - { - "mac": 176156956989009, - "rawRssi": -49 - }, - { - "mac": 236032111888056, - "rawRssi": -63 - }, - { - "mac": 5175406356059, - "rawRssi": -69 - }, - { - "mac": 233833088632553, - "rawRssi": -76 - }, - { - "mac": 233833088632552, - "rawRssi": -63 - }, - { - "mac": 13685719791895, - "rawRssi": -83 - }, - { - "mac": 5175406356058, - "rawRssi": -85 - }, - { - "mac": 88949690804935, - "rawRssi": -78 - }, - { - "mac": 196031866815400, - "rawRssi": -58 - }, - { - "mac": 66692969804511, - "rawRssi": -85 - }, - { - "mac": 31806095594931, - "rawRssi": -87 - }, - { - "mac": 79565174465540, - "rawRssi": -78 - }, - { - "mac": 207775459763230, - "rawRssi": -71 - }, - { - "mac": 272875381228876, - "rawRssi": -70 - }, - { - "mac": 236032111888024, - "rawRssi": -64 - }, - { - "mac": 207775459763231, - "rawRssi": -77 - }, - { - "mac": 272875381228877, - "rawRssi": -83 - }, - { - "mac": 187152073266777, - "rawRssi": -52 - }, - { - "mac": 251751037760296, - "rawRssi": -67 - }, - { - "mac": 251751037760295, - "rawRssi": -60 - }, - { - "mac": 253950061015831, - "rawRssi": -63 - }, - { - "mac": 176353769414803, - "rawRssi": -72 - }, - { - "mac": 13685728359545, - "rawRssi": -75 - }, - { - "mac": 253950061015863, - "rawRssi": -60 - }, - { - "mac": 225185649798751, - "rawRssi": -80 - }, - { - "mac": 176156956988992, - "rawRssi": -49 - }, - { - "mac": 176156956988996, - "rawRssi": -37 - }, - { - "mac": 245000064254810, - "rawRssi": -74 - } - ] - } - }, - { - "position": { - "LatLng": "53.863285°, 10.7011163°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:45:51.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632851°, 10.7011164°", - "accuracyMeters": 23, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:46:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632853°, 10.7011164°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:47:07.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7011165°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:47:44.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011164°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:48:20.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632862°, 10.7011166°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:48:57.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.7011165°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:49:34.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011164°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:50:11.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.7011162°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:50:49.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.701116°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:51:23.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632862°, 10.701116°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:51:59.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632862°, 10.7011161°", - "accuracyMeters": 22, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:52:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011162°", - "accuracyMeters": 25, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:53:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011162°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:53:51.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011163°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:54:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863247°, 10.7011031°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:55:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863272°, 10.7010812°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:55:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.701116°", - "accuracyMeters": 21, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:56:25.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T11:56:43.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -91 - }, - { - "mac": 81764197721092, - "rawRssi": -75 - }, - { - "mac": 158820375294976, - "rawRssi": -81 - }, - { - "mac": 4880535074783, - "rawRssi": -67 - }, - { - "mac": 176353769414806, - "rawRssi": -75 - }, - { - "mac": 4880535074782, - "rawRssi": -62 - }, - { - "mac": 162987945711698, - "rawRssi": -91 - }, - { - "mac": 123486069126043, - "rawRssi": -73 - }, - { - "mac": 13685728359546, - "rawRssi": -89 - }, - { - "mac": 13685719791891, - "rawRssi": -80 - }, - { - "mac": 13685723516731, - "rawRssi": -81 - }, - { - "mac": 176156956989012, - "rawRssi": -49 - }, - { - "mac": 176156956989009, - "rawRssi": -54 - }, - { - "mac": 5175406356059, - "rawRssi": -73 - }, - { - "mac": 233833088632553, - "rawRssi": -81 - }, - { - "mac": 233833088632552, - "rawRssi": -61 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -76 - }, - { - "mac": 196031866815400, - "rawRssi": -67 - }, - { - "mac": 66692969804511, - "rawRssi": -80 - }, - { - "mac": 103893075959292, - "rawRssi": -85 - }, - { - "mac": 101694052703744, - "rawRssi": -88 - }, - { - "mac": 79565174465540, - "rawRssi": -82 - }, - { - "mac": 79565174465541, - "rawRssi": -92 - }, - { - "mac": 207775459763230, - "rawRssi": -54 - }, - { - "mac": 272875381228876, - "rawRssi": -63 - }, - { - "mac": 236032111888024, - "rawRssi": -62 - }, - { - "mac": 207775459763231, - "rawRssi": -68 - }, - { - "mac": 272875381228877, - "rawRssi": -84 - }, - { - "mac": 251751037760296, - "rawRssi": -67 - }, - { - "mac": 253950061015831, - "rawRssi": -68 - }, - { - "mac": 162987945711695, - "rawRssi": -91 - }, - { - "mac": 176353769414803, - "rawRssi": -65 - }, - { - "mac": 176156956988992, - "rawRssi": -37 - }, - { - "mac": 162987945711694, - "rawRssi": -85 - }, - { - "mac": 176156956988996, - "rawRssi": -36 - }, - { - "mac": 245000064254810, - "rawRssi": -72 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.7011156°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:57:05.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632858°, 10.7011155°", - "accuracyMeters": 27, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:57:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632859°, 10.7011155°", - "accuracyMeters": 27, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:58:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633018°, 10.7010971°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:58:57.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633095°, 10.701065°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T11:59:34.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632873°, 10.7010841°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:00:12.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011152°", - "accuracyMeters": 23, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:00:56.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863285°, 10.7010875°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:01:33.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633047°, 10.701095°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:02:17.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632856°, 10.7011145°", - "accuracyMeters": 20, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:03:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632959°, 10.7010585°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:03:37.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.863286°, 10.7011132°", - "accuracyMeters": 16, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:04:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:04:50.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633612°, 10.7011268°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:05:02.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633335°, 10.7010918°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:05:46.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:06:08.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632741°, 10.7010825°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:06:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:06:53.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863298°, 10.7010531°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:07:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T12:07:38.000+02:00", - "devicesRecords": [ - { - "mac": 158820375294976, - "rawRssi": -76 - }, - { - "mac": 4880535074783, - "rawRssi": -90 - }, - { - "mac": 176353769414806, - "rawRssi": -68 - }, - { - "mac": 62533885077, - "rawRssi": -87 - }, - { - "mac": 207775459763234, - "rawRssi": -87 - }, - { - "mac": 53539816085391, - "rawRssi": -87 - }, - { - "mac": 49004153403042, - "rawRssi": -83 - }, - { - "mac": 48631873941199, - "rawRssi": -93 - }, - { - "mac": 88949681086751, - "rawRssi": -92 - }, - { - "mac": 176156956989012, - "rawRssi": -82 - }, - { - "mac": 101694052703736, - "rawRssi": -74 - }, - { - "mac": 53539816085394, - "rawRssi": -87 - }, - { - "mac": 176156956989009, - "rawRssi": -73 - }, - { - "mac": 233833088632553, - "rawRssi": -89 - }, - { - "mac": 79965018526574, - "rawRssi": -87 - }, - { - "mac": 31806095594932, - "rawRssi": -83 - }, - { - "mac": 196031866815400, - "rawRssi": -77 - }, - { - "mac": 103893075959290, - "rawRssi": -74 - }, - { - "mac": 103893075959292, - "rawRssi": -74 - }, - { - "mac": 101694052703744, - "rawRssi": -72 - }, - { - "mac": 57388349528149, - "rawRssi": -80 - }, - { - "mac": 207775459763231, - "rawRssi": -87 - }, - { - "mac": 187152073266777, - "rawRssi": -83 - }, - { - "mac": 242139249841737, - "rawRssi": -92 - }, - { - "mac": 251751037760296, - "rawRssi": -88 - }, - { - "mac": 242139249841736, - "rawRssi": -75 - }, - { - "mac": 241986117947288, - "rawRssi": -93 - }, - { - "mac": 176353769414803, - "rawRssi": -70 - }, - { - "mac": 62533885075, - "rawRssi": -72 - }, - { - "mac": 176156956988992, - "rawRssi": -83 - }, - { - "mac": 66207235812212, - "rawRssi": -87 - }, - { - "mac": 176156956988996, - "rawRssi": -80 - }, - { - "mac": 245000064254810, - "rawRssi": -61 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632511°, 10.7012395°", - "accuracyMeters": 5, - "altitudeMeters": 51.39999771118164, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:07:54.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9300000071525574 - }, - { - "type": "WALKING", - "confidence": 0.9300000071525574 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "STILL", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.009999999776482582 - }, - { - "type": "RUNNING", - "confidence": 0.009999999776482582 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:08:07.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632409°, 10.7012973°", - "accuracyMeters": 17, - "altitudeMeters": 48.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:08:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632123°, 10.702345°", - "accuracyMeters": 7, - "altitudeMeters": 48.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:09:04.000+02:00", - "speedMetersPerSecond": 0.44999998807907104 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9200000166893005 - }, - { - "type": "WALKING", - "confidence": 0.9200000166893005 - }, - { - "type": "STILL", - "confidence": 0.029999999329447746 - }, - { - "type": "RUNNING", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.019999999552965164 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:09:17.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632377°, 10.7018977°", - "accuracyMeters": 9, - "altitudeMeters": 48.599998474121094, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:09:38.000+02:00", - "speedMetersPerSecond": 0.3700000047683716 - } - }, - { - "position": { - "LatLng": "53.8631522°, 10.701307°", - "accuracyMeters": 6, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:10:12.000+02:00", - "speedMetersPerSecond": 0.5 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "ON_FOOT", - "confidence": 0.9300000071525574 - }, - { - "type": "WALKING", - "confidence": 0.9300000071525574 - }, - { - "type": "STILL", - "confidence": 0.029999999329447746 - }, - { - "type": "UNKNOWN", - "confidence": 0.029999999329447746 - }, - { - "type": "RUNNING", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:10:25.000+02:00" - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T12:10:27.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -87 - }, - { - "mac": 4880535074782, - "rawRssi": -81 - }, - { - "mac": 177018967513232, - "rawRssi": -80 - }, - { - "mac": 48631873941199, - "rawRssi": -93 - }, - { - "mac": 128164988653540, - "rawRssi": -87 - }, - { - "mac": 128164988653541, - "rawRssi": -76 - }, - { - "mac": 178355980244552, - "rawRssi": -67 - }, - { - "mac": 4880535074786, - "rawRssi": -88 - }, - { - "mac": 83681112333254, - "rawRssi": -93 - }, - { - "mac": 128164988653537, - "rawRssi": -69 - }, - { - "mac": 242945278662559, - "rawRssi": -92 - }, - { - "mac": 83681112333251, - "rawRssi": -95 - }, - { - "mac": 79965018526574, - "rawRssi": -82 - }, - { - "mac": 88673306087965, - "rawRssi": -88 - }, - { - "mac": 79565174465540, - "rawRssi": -81 - }, - { - "mac": 272875381228877, - "rawRssi": -91 - }, - { - "mac": 242139249841737, - "rawRssi": -92 - }, - { - "mac": 241986117947284, - "rawRssi": -74 - }, - { - "mac": 242139249841736, - "rawRssi": -75 - }, - { - "mac": 241986117947288, - "rawRssi": -93 - }, - { - "mac": 225185649798751, - "rawRssi": -74 - }, - { - "mac": 113026945773536, - "rawRssi": -79 - }, - { - "mac": 113026945773538, - "rawRssi": -83 - }, - { - "mac": 113026945773537, - "rawRssi": -82 - }, - { - "mac": 57623789444075, - "rawRssi": -77 - }, - { - "mac": 113026945773539, - "rawRssi": -83 - }, - { - "mac": 137435962226968, - "rawRssi": -72 - }, - { - "mac": 53539816085391, - "rawRssi": -87 - }, - { - "mac": 53539816085390, - "rawRssi": -84 - }, - { - "mac": 136877347993184, - "rawRssi": -85 - }, - { - "mac": 101694052703736, - "rawRssi": -74 - }, - { - "mac": 53539816085394, - "rawRssi": -87 - }, - { - "mac": 233833088632553, - "rawRssi": -89 - }, - { - "mac": 233833088632552, - "rawRssi": -59 - }, - { - "mac": 31806105158605, - "rawRssi": -76 - }, - { - "mac": 31806095594932, - "rawRssi": -83 - }, - { - "mac": 103893075959290, - "rawRssi": -74 - }, - { - "mac": 31806095594931, - "rawRssi": -87 - }, - { - "mac": 103893075959292, - "rawRssi": -74 - }, - { - "mac": 128164983198404, - "rawRssi": -83 - }, - { - "mac": 50956825402769, - "rawRssi": -86 - }, - { - "mac": 128164983198398, - "rawRssi": -79 - }, - { - "mac": 128164983198399, - "rawRssi": -85 - }, - { - "mac": 110827922517988, - "rawRssi": -69 - }, - { - "mac": 110827922517987, - "rawRssi": -81 - }, - { - "mac": 85880135588803, - "rawRssi": -92 - }, - { - "mac": 185773116974064, - "rawRssi": -83 - }, - { - "mac": 110827922517984, - "rawRssi": -84 - }, - { - "mac": 154042044640441, - "rawRssi": -75 - }, - { - "mac": 158820375294976, - "rawRssi": -81 - } - ] - } - }, - { - "position": { - "LatLng": "53.8632131°, 10.7009057°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:10:45.000+02:00", - "speedMetersPerSecond": 0.5699999928474426 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:11:21.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633051°, 10.7010892°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:11:21.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T12:11:24.000+02:00", - "devicesRecords": [ - { - "mac": 81764197721093, - "rawRssi": -93 - }, - { - "mac": 81764197721092, - "rawRssi": -82 - }, - { - "mac": 4880535074783, - "rawRssi": -69 - }, - { - "mac": 176353769414806, - "rawRssi": -76 - }, - { - "mac": 4880535074782, - "rawRssi": -61 - }, - { - "mac": 123486069126043, - "rawRssi": -78 - }, - { - "mac": 203326496528334, - "rawRssi": -85 - }, - { - "mac": 198996529455448, - "rawRssi": -68 - }, - { - "mac": 13685719791891, - "rawRssi": -88 - }, - { - "mac": 198996529455449, - "rawRssi": -80 - }, - { - "mac": 233254798443902, - "rawRssi": -90 - }, - { - "mac": 233254798443903, - "rawRssi": -76 - }, - { - "mac": 176156956989012, - "rawRssi": -55 - }, - { - "mac": 101694052703736, - "rawRssi": -85 - }, - { - "mac": 4880535074786, - "rawRssi": -70 - }, - { - "mac": 176156956989009, - "rawRssi": -71 - }, - { - "mac": 5175406356059, - "rawRssi": -83 - }, - { - "mac": 233833088632552, - "rawRssi": -78 - }, - { - "mac": 246326394024491, - "rawRssi": -87 - }, - { - "mac": 196031866815400, - "rawRssi": -70 - }, - { - "mac": 103893075959290, - "rawRssi": -86 - }, - { - "mac": 101694052703744, - "rawRssi": -83 - }, - { - "mac": 79565174465540, - "rawRssi": -82 - }, - { - "mac": 79565174465541, - "rawRssi": -92 - }, - { - "mac": 207775459763230, - "rawRssi": -62 - }, - { - "mac": 207775459763231, - "rawRssi": -63 - }, - { - "mac": 236032111888024, - "rawRssi": -79 - }, - { - "mac": 187152073266777, - "rawRssi": -55 - }, - { - "mac": 251751037760296, - "rawRssi": -82 - }, - { - "mac": 251751037760295, - "rawRssi": -84 - }, - { - "mac": 253950061015831, - "rawRssi": -83 - }, - { - "mac": 176353769414803, - "rawRssi": -71 - }, - { - "mac": 253950061015863, - "rawRssi": -84 - }, - { - "mac": 176156956988992, - "rawRssi": -69 - }, - { - "mac": 176156956988996, - "rawRssi": -46 - }, - { - "mac": 245000064254810, - "rawRssi": -67 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T12:11:53.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633197°, 10.7010906°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:12:03.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:12:25.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633076°, 10.7011068°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:12:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:13:30.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632984°, 10.7011114°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:13:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632953°, 10.7011116°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:14:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.8100000023841858 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.11999999731779099 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.11999999731779099 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.029999999329447746 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.029999999329447746 - }, - { - "type": "ON_FOOT", - "confidence": 0.019999999552965164 - }, - { - "type": "WALKING", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.019999999552965164 - } - ], - "timestamp": "2025-07-04T12:14:36.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633022°, 10.7010974°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:14:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:14:59.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632968°, 10.7010849°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:15:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:16:04.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633032°, 10.7010886°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:16:26.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:17:08.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632877°, 10.7011005°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:17:08.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T12:17:39.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863289°, 10.7011046°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:17:50.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:18:11.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633417°, 10.7010404°", - "accuracyMeters": 18, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:18:32.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.5199999809265137 - }, - { - "type": "ON_FOOT", - "confidence": 0.36000001430511475 - }, - { - "type": "WALKING", - "confidence": 0.36000001430511475 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.05000000074505806 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.05000000074505806 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.05000000074505806 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.029999999329447746 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.019999999552965164 - }, - { - "type": "RUNNING", - "confidence": 0.009999999776482582 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:19:16.000+02:00" - } - }, - { - "position": { - "LatLng": "53.863325°, 10.7011119°", - "accuracyMeters": 10, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:19:16.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633767°, 10.7011028°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:20:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:20:22.000+02:00" - } - }, - { - "position": { - "LatLng": "53.86335°, 10.7010312°", - "accuracyMeters": 17, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:20:44.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9800000190734863 - }, - { - "type": "ON_FOOT", - "confidence": 0.009999999776482582 - }, - { - "type": "WALKING", - "confidence": 0.009999999776482582 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:21:29.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633541°, 10.7010566°", - "accuracyMeters": 15, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:21:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8633621°, 10.7010672°", - "accuracyMeters": 15, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:22:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9700000286102295 - }, - { - "type": "ON_FOOT", - "confidence": 0.019999999552965164 - }, - { - "type": "WALKING", - "confidence": 0.019999999552965164 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:22:36.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633281°, 10.7009964°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:22:58.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T12:23:09.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7010918°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:23:35.000+02:00", - "speedMetersPerSecond": 0.3896327018737793 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T12:23:35.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -72 - }, - { - "mac": 176353769414806, - "rawRssi": -80 - }, - { - "mac": 4880535074782, - "rawRssi": -63 - }, - { - "mac": 207775459763234, - "rawRssi": -73 - }, - { - "mac": 123486069126043, - "rawRssi": -78 - }, - { - "mac": 233254798443903, - "rawRssi": -83 - }, - { - "mac": 176156956989012, - "rawRssi": -61 - }, - { - "mac": 176156956989009, - "rawRssi": -64 - }, - { - "mac": 5175406356059, - "rawRssi": -75 - }, - { - "mac": 233833088632553, - "rawRssi": -82 - }, - { - "mac": 233833088632552, - "rawRssi": -72 - }, - { - "mac": 13685719791895, - "rawRssi": -85 - }, - { - "mac": 5175406356058, - "rawRssi": -83 - }, - { - "mac": 196031866815400, - "rawRssi": -67 - }, - { - "mac": 207775459763230, - "rawRssi": -61 - }, - { - "mac": 207775459763231, - "rawRssi": -73 - }, - { - "mac": 272875381228877, - "rawRssi": -90 - }, - { - "mac": 187152073266777, - "rawRssi": -61 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -66 - }, - { - "mac": 253950061015831, - "rawRssi": -66 - }, - { - "mac": 176353769414803, - "rawRssi": -74 - }, - { - "mac": 13685728359545, - "rawRssi": -82 - }, - { - "mac": 253950061015863, - "rawRssi": -65 - }, - { - "mac": 176156956988992, - "rawRssi": -35 - }, - { - "mac": 162987945711694, - "rawRssi": -83 - }, - { - "mac": 176156956988996, - "rawRssi": -32 - }, - { - "mac": 245000064254810, - "rawRssi": -66 - } - ] - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:23:41.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633017°, 10.7010952°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:24:23.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:24:45.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632964°, 10.7010961°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:25:07.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:25:51.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632962°, 10.7010831°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:25:51.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632955°, 10.7010949°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:26:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:26:57.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632967°, 10.7010947°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:27:19.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:27:42.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8633009°, 10.7010987°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:28:03.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:28:47.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632936°, 10.7010907°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:28:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632983°, 10.7010933°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:29:30.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9700000286102295 - }, - { - "type": "UNKNOWN", - "confidence": 0.029999999329447746 - } - ], - "timestamp": "2025-07-04T12:29:52.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632942°, 10.7011123°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:30:14.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:30:57.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632748°, 10.7010956°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:30:57.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632935°, 10.7011094°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:31:39.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:32:01.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632922°, 10.7010887°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:32:22.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:32:23.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T12:32:54.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632861°, 10.7011189°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:33:04.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "UNKNOWN", - "confidence": 0.4000000059604645 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_BICYCLE", - "confidence": 0.10000000149011612 - }, - { - "type": "ON_FOOT", - "confidence": 0.10000000149011612 - }, - { - "type": "WALKING", - "confidence": 0.10000000149011612 - }, - { - "type": "RUNNING", - "confidence": 0.10000000149011612 - }, - { - "type": "STILL", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.10000000149011612 - }, - { - "type": "IN_ROAD_VEHICLE", - "confidence": 0.10000000149011612 - } - ], - "timestamp": "2025-07-04T12:33:26.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632765°, 10.7011239°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:33:47.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "TILTING", - "confidence": 1.0 - } - ], - "timestamp": "2025-07-04T12:33:57.000+02:00" - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9599999785423279 - }, - { - "type": "UNKNOWN", - "confidence": 0.019999999552965164 - }, - { - "type": "IN_VEHICLE", - "confidence": 0.009999999776482582 - }, - { - "type": "ON_FOOT", - "confidence": 0.009999999776482582 - }, - { - "type": "WALKING", - "confidence": 0.009999999776482582 - }, - { - "type": "IN_RAIL_VEHICLE", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:34:29.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632919°, 10.7010953°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:34:29.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "wifiScan": { - "deliveryTime": "2025-07-04T12:34:46.000+02:00", - "devicesRecords": [ - { - "mac": 4880535074783, - "rawRssi": -73 - }, - { - "mac": 176353769414806, - "rawRssi": -81 - }, - { - "mac": 4880535074782, - "rawRssi": -60 - }, - { - "mac": 123486069126043, - "rawRssi": -82 - }, - { - "mac": 79565160561132, - "rawRssi": -88 - }, - { - "mac": 13685719791891, - "rawRssi": -70 - }, - { - "mac": 176156956989012, - "rawRssi": -58 - }, - { - "mac": 4880535074786, - "rawRssi": -74 - }, - { - "mac": 176156956989009, - "rawRssi": -58 - }, - { - "mac": 5175406356059, - "rawRssi": -73 - }, - { - "mac": 233833088632553, - "rawRssi": -83 - }, - { - "mac": 13685719791895, - "rawRssi": -84 - }, - { - "mac": 5175406356058, - "rawRssi": -85 - }, - { - "mac": 88949690804935, - "rawRssi": -83 - }, - { - "mac": 101694052703744, - "rawRssi": -92 - }, - { - "mac": 207775459763230, - "rawRssi": -70 - }, - { - "mac": 207775459763231, - "rawRssi": -75 - }, - { - "mac": 187152073266777, - "rawRssi": -58 - }, - { - "mac": 251751037760296, - "rawRssi": -72 - }, - { - "mac": 251751037760295, - "rawRssi": -76 - }, - { - "mac": 253950061015831, - "rawRssi": -75 - }, - { - "mac": 176156956988992, - "rawRssi": -44 - }, - { - "mac": 162987945711694, - "rawRssi": -77 - }, - { - "mac": 176156956988996, - "rawRssi": -34 - }, - { - "mac": 245000064254810, - "rawRssi": -74 - } - ] - } - }, - { - "position": { - "LatLng": "53.863288°, 10.7010788°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:35:11.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:35:32.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632922°, 10.7010924°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:35:53.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:36:35.000+02:00" - } - }, - { - "position": { - "LatLng": "53.86329°, 10.7010967°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:36:35.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632961°, 10.7010794°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:37:18.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:37:39.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632945°, 10.7010824°", - "accuracyMeters": 12, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:38:00.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "activityRecord": { - "probableActivities": [ - { - "type": "STILL", - "confidence": 0.9900000095367432 - }, - { - "type": "UNKNOWN", - "confidence": 0.009999999776482582 - } - ], - "timestamp": "2025-07-04T12:38:42.000+02:00" - } - }, - { - "position": { - "LatLng": "53.8632956°, 10.7010816°", - "accuracyMeters": 11, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:38:42.000+02:00", - "speedMetersPerSecond": 0.0 - } - }, - { - "position": { - "LatLng": "53.8632725°, 10.7010533°", - "accuracyMeters": 13, - "altitudeMeters": 53.70000076293945, - "source": "UNKNOWN", - "timestamp": "2025-07-04T12:39:24.000+02:00", - "speedMetersPerSecond": 0.0 - } - } - ], - "userLocationProfile": {} -} \ No newline at end of file diff --git a/src/test/resources/data/google/tl_randomized.json b/src/test/resources/data/google/timeline_from_android_randomized.json similarity index 100% rename from src/test/resources/data/google/tl_randomized.json rename to src/test/resources/data/google/timeline_from_android_randomized.json diff --git a/src/test/resources/data/google/timeline_from_ios_randomized.json b/src/test/resources/data/google/timeline_from_ios_randomized.json new file mode 100644 index 00000000..90a0cd38 --- /dev/null +++ b/src/test/resources/data/google/timeline_from_ios_randomized.json @@ -0,0 +1,627 @@ +[ { + "endTime" : "2023-12-27T13:39:37+01:00", + "startTime" : "2023-12-27T13:32:18+01:00", + "activity" : { + "end" : "geo:55.6054870,24.7648727", + "topCandidate" : { + "type" : "in passenger vehicle", + "probability" : "0.000000" + }, + "distanceMeters" : "1881.000000", + "start" : "geo:55.5963490,24.7405147" + } +}, { + "endTime" : "2023-12-28T05:43:23+01:00", + "startTime" : "2023-12-27T13:39:37+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.950077", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2023-12-28T06:15:12+01:00", + "startTime" : "2023-12-28T05:43:23+01:00", + "activity" : { + "end" : "geo:55.6059620,24.7648227", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "2996.000000", + "start" : "geo:55.6054810,24.7645917" + } +}, { + "endTime" : "2023-12-29T04:25:02+01:00", + "startTime" : "2023-12-28T06:15:12+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.882598", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.960000" + } +}, { + "endTime" : "2023-12-29T04:52:49+01:00", + "startTime" : "2023-12-29T04:25:02+01:00", + "activity" : { + "end" : "geo:55.6061620,24.7581767", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "3002.000000", + "start" : "geo:55.6055850,24.7651717" + } +}, { + "endTime" : "2023-12-29T05:11:04+01:00", + "startTime" : "2023-12-29T04:52:49+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.379193", + "semanticType" : "Unknown", + "placeID" : "ChIJH_q0TvyjU0YRqIoh_gCjDT8", + "placeLocation" : "geo:55.6060130,24.7587297" + }, + "probability" : "0.850000" + } +}, { + "endTime" : "2023-12-29T05:12:33+01:00", + "startTime" : "2023-12-29T05:11:04+01:00", + "activity" : { + "end" : "geo:55.6058550,24.7648057", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "318.000000", + "start" : "geo:55.6062470,24.7597747" + } +}, { + "endTime" : "2023-12-30T01:17:48+01:00", + "startTime" : "2023-12-29T05:12:33+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.940788", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.880000" + } +}, { + "endTime" : "2023-12-30T01:21:40+01:00", + "startTime" : "2023-12-30T01:17:48+01:00", + "activity" : { + "end" : "geo:55.6010460,24.7577547", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "686.000000", + "start" : "geo:55.6056070,24.7651197" + } +}, { + "endTime" : "2023-12-30T02:01:59+01:00", + "startTime" : "2023-12-30T01:21:40+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.214051", + "semanticType" : "Unknown", + "placeID" : "ChIJOREJWQOhU0YR3-eyX-tz6fY", + "placeLocation" : "geo:55.5994210,24.7551007" + }, + "probability" : "0.860000" + } +}, { + "endTime" : "2023-12-30T02:27:00+01:00", + "startTime" : "2023-12-30T02:01:59+01:00", + "activity" : { + "end" : "geo:55.6046750,24.7653237", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1957.000000", + "start" : "geo:55.5993930,24.7554737" + } +}, { + "endTime" : "2023-12-31T00:42:02+01:00", + "startTime" : "2023-12-30T02:27:00+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.957793", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.930000" + } +}, { + "endTime" : "2023-12-31T00:46:44+01:00", + "startTime" : "2023-12-31T00:42:02+01:00", + "activity" : { + "end" : "geo:55.6101320,24.7581277", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "716.000000", + "start" : "geo:55.6056180,24.7651577" + } +}, { + "endTime" : "2023-12-31T01:06:30+01:00", + "startTime" : "2023-12-31T00:46:44+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.512187", + "semanticType" : "Unknown", + "placeID" : "ChIJB9rdBvmjU0YRGyaRmRCgYNE", + "placeLocation" : "geo:55.6090670,24.7571257" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2023-12-31T01:09:18+01:00", + "startTime" : "2023-12-31T01:06:30+01:00", + "activity" : { + "end" : "geo:55.6072080,24.7550407", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "352.000000", + "start" : "geo:55.6098680,24.7581087" + } +}, { + "endTime" : "2023-12-31T01:47:42+01:00", + "startTime" : "2023-12-31T01:09:18+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.952757", + "semanticType" : "Unknown", + "placeID" : "ChIJD577ZP-jU0YRoI5DT318s3s", + "placeLocation" : "geo:55.6065880,24.7537857" + }, + "probability" : "0.930000" + } +}, { + "endTime" : "2023-12-31T02:05:03+01:00", + "startTime" : "2023-12-31T01:47:42+01:00", + "activity" : { + "end" : "geo:55.6042040,24.7658087", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1305.000000", + "start" : "geo:55.6066710,24.7538547" + } +}, { + "endTime" : "2023-12-31T07:17:30+01:00", + "startTime" : "2023-12-31T02:05:03+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.960007", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.870000" + } +}, { + "endTime" : "2023-12-31T07:20:27+01:00", + "startTime" : "2023-12-31T07:17:30+01:00", + "activity" : { + "end" : "geo:55.5972900,24.7604017", + "topCandidate" : { + "type" : "cycling", + "probability" : "0.000000" + }, + "distanceMeters" : "1088.000000", + "start" : "geo:55.6055780,24.7651217" + } +}, { + "endTime" : "2023-12-31T15:51:45+01:00", + "startTime" : "2023-12-31T07:20:27+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.956829", + "semanticType" : "Unknown", + "placeID" : "ChIJheyEwFihU0YRQqqvcTVghbI", + "placeLocation" : "geo:55.5971680,24.7599527" + }, + "probability" : "0.920000" + } +}, { + "endTime" : "2023-12-31T15:53:15+01:00", + "startTime" : "2023-12-31T15:51:45+01:00", + "activity" : { + "end" : "geo:55.6059150,24.7650917", + "topCandidate" : { + "type" : "in passenger vehicle", + "probability" : "0.000000" + }, + "distanceMeters" : "1017.000000", + "start" : "geo:55.5972370,24.7599447" + } +}, { + "endTime" : "2023-12-31T23:45:41+01:00", + "startTime" : "2023-12-31T15:53:15+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.964839", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.930000" + } +}, { + "endTime" : "2023-12-31T23:51:00+01:00", + "startTime" : "2023-12-31T23:45:41+01:00", + "activity" : { + "end" : "geo:55.6065440,24.7538257", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "733.000000", + "start" : "geo:55.6055840,24.7651497" + } +}, { + "endTime" : "2024-01-01T00:39:49+01:00", + "startTime" : "2023-12-31T23:51:00+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.853651", + "semanticType" : "Unknown", + "placeID" : "ChIJlWPd-6umU0YRr_ka5wn9syA", + "placeLocation" : "geo:55.6063990,24.7538237" + }, + "probability" : "0.960000" + } +}, { + "endTime" : "2024-01-01T00:47:11+01:00", + "startTime" : "2024-01-01T00:39:49+01:00", + "activity" : { + "end" : "geo:55.5989250,24.7581417", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "906.000000", + "start" : "geo:55.6066500,24.7537077" + } +}, { + "endTime" : "2024-01-01T00:51:21+01:00", + "startTime" : "2024-01-01T00:47:11+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.712165", + "semanticType" : "Unknown", + "placeID" : "ChIJ6fhxFFehU0YRQdHb-n1urnU", + "placeLocation" : "geo:55.5987410,24.7584377" + }, + "probability" : "0.790000" + } +}, { + "endTime" : "2024-01-01T00:56:40+01:00", + "startTime" : "2024-01-01T00:51:21+01:00", + "activity" : { + "end" : "geo:55.5951970,24.7585717", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "402.000000", + "start" : "geo:55.5987860,24.7581707" + } +}, { + "endTime" : "2024-01-01T02:50:57+01:00", + "startTime" : "2024-01-01T00:56:40+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.893793", + "semanticType" : "Unknown", + "placeID" : "ChIJZ4us_FChU0YR40LSw4OSfXQ", + "placeLocation" : "geo:55.5950560,24.7564317" + }, + "probability" : "0.850000" + } +}, { + "endTime" : "2024-01-01T02:53:01+01:00", + "startTime" : "2024-01-01T02:50:57+01:00", + "activity" : { + "end" : "geo:55.5980710,24.7586277", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "374.000000", + "start" : "geo:55.5948170,24.7570757" + } +}, { + "endTime" : "2024-01-01T03:09:35+01:00", + "startTime" : "2024-01-01T02:53:01+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.201525", + "semanticType" : "Unknown", + "placeID" : "ChIJ6fhxFFehU0YRQdHb-n1urnU", + "placeLocation" : "geo:55.5987410,24.7584377" + }, + "probability" : "0.810000" + } +}, { + "endTime" : "2024-01-01T03:20:35+01:00", + "startTime" : "2024-01-01T03:09:35+01:00", + "activity" : { + "end" : "geo:55.6049390,24.7652387", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1057.000000", + "start" : "geo:55.5976370,24.7585717" + } +}, { + "endTime" : "2024-01-02T03:30:33+01:00", + "startTime" : "2024-01-01T03:20:35+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.937964", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2024-01-02T03:42:48+01:00", + "startTime" : "2024-01-02T03:30:33+01:00", + "activity" : { + "end" : "geo:55.5946320,24.7600697", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1427.000000", + "start" : "geo:55.6056870,24.7646217" + } +}, { + "endTime" : "2024-01-02T04:18:42+01:00", + "startTime" : "2024-01-02T03:42:48+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.935643", + "semanticType" : "Unknown", + "placeID" : "ChIJzdglClqhU0YRqFN_6F8BR3s", + "placeLocation" : "geo:55.5956530,24.7593017" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2024-01-02T04:18:42+01:00", + "startTime" : "2024-01-02T03:42:48+01:00", + "visit" : { + "hierarchyLevel" : "1", + "topCandidate" : { + "probability" : "0.194063", + "semanticType" : "Unknown", + "placeID" : "ChIJPT2i9FChU0YR-6zMtgh5zYE", + "placeLocation" : "geo:55.5947380,24.7598317" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2024-01-02T04:30:01+01:00", + "startTime" : "2024-01-02T04:18:42+01:00", + "activity" : { + "end" : "geo:55.6054340,24.7650557", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1294.000000", + "start" : "geo:55.5952460,24.7595977" + } +}, { + "endTime" : "2024-01-03T03:45:11+01:00", + "startTime" : "2024-01-02T04:30:01+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.935800", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2024-01-03T04:23:02+01:00", + "startTime" : "2024-01-03T03:45:11+01:00", + "activity" : { + "end" : "geo:55.6054660,24.7651577", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "3863.000000", + "start" : "geo:55.6055960,24.7651657" + } +}, { + "endTime" : "2024-01-03T21:35:43+01:00", + "startTime" : "2024-01-03T04:23:02+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.951770", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.950000" + } +}, { + "endTime" : "2024-01-03T21:44:52+01:00", + "startTime" : "2024-01-03T21:35:43+01:00", + "activity" : { + "end" : "geo:55.5999710,24.7581077", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1306.000000", + "start" : "geo:55.6056100,24.7651657" + } +}, { + "endTime" : "2024-01-03T23:08:37+01:00", + "startTime" : "2024-01-03T21:44:52+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.418631", + "semanticType" : "Work", + "placeID" : "ChIJyfwWFlehU0YR0VHiLT7T9pE", + "placeLocation" : "geo:55.5985370,24.7580897" + }, + "probability" : "0.960000" + } +}, { + "endTime" : "2024-01-03T23:16:04+01:00", + "startTime" : "2024-01-03T23:08:37+01:00", + "activity" : { + "end" : "geo:55.6055610,24.7654167", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "902.000000", + "start" : "geo:55.5986990,24.7578647" + } +}, { + "endTime" : "2024-01-03T23:22:47+01:00", + "startTime" : "2024-01-03T23:16:04+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.959538", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.840000" + } +}, { + "endTime" : "2024-01-03T23:29:07+01:00", + "startTime" : "2024-01-03T23:22:47+01:00", + "activity" : { + "end" : "geo:55.6021610,24.7577367", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "1000.000000", + "start" : "geo:55.6049340,24.7635937" + } +}, { + "endTime" : "2024-01-03T23:35:08+01:00", + "startTime" : "2024-01-03T23:29:07+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.684810", + "semanticType" : "Unknown", + "placeID" : "ChIJDwpg1ZShU0YRyeVrQ2hzcpc", + "placeLocation" : "geo:55.6020610,24.7579257" + }, + "probability" : "0.820000" + } +}, { + "endTime" : "2024-01-03T23:36:34+01:00", + "startTime" : "2024-01-03T23:35:08+01:00", + "activity" : { + "end" : "geo:55.5987060,24.7583947", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "470.000000", + "start" : "geo:55.6029350,24.7581777" + } +}, { + "endTime" : "2024-01-04T06:37:22+01:00", + "startTime" : "2024-01-03T23:36:34+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.344615", + "semanticType" : "Work", + "placeID" : "ChIJyfwWFlehU0YR0VHiLT7T9pE", + "placeLocation" : "geo:55.5985370,24.7580897" + }, + "probability" : "0.920000" + } +}, { + "endTime" : "2024-01-04T06:45:55+01:00", + "startTime" : "2024-01-04T06:37:22+01:00", + "activity" : { + "end" : "geo:55.6056190,24.7646597", + "topCandidate" : { + "type" : "walking", + "probability" : "0.000000" + }, + "distanceMeters" : "983.000000", + "start" : "geo:55.5985560,24.7579737" + } +}, { + "endTime" : "2024-01-04T20:58:43+01:00", + "startTime" : "2024-01-04T06:45:55+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.965214", + "semanticType" : "Home", + "placeID" : "ChIJ46eCM-OjU0YRApSt64ssrck", + "placeLocation" : "geo:55.6058430,24.7647107" + }, + "probability" : "0.960000" + } +}, { + "endTime" : "2024-01-04T21:05:36+01:00", + "startTime" : "2024-01-04T20:58:43+01:00", + "visit" : { + "hierarchyLevel" : "0", + "topCandidate" : { + "probability" : "0.811692", + "semanticType" : "Unknown", + "placeID" : "ChIJB9rdBvmjU0YRGyaRmRCgYNE", + "placeLocation" : "geo:55.6090670,24.7571257" + }, + "probability" : "0.560000" + } +} ] \ No newline at end of file