Jelajahi Sumber

Added support for GeoJSON

Lukas Cerny 1 tahun lalu
induk
melakukan
72b20726d4

+ 4 - 4
src/main/java/cz/senslog/telemetry/database/domain/SensorTelemetry.java

@@ -5,16 +5,16 @@ import java.time.OffsetDateTime;
 public class SensorTelemetry {
 
     private final long id;
-    private final Long value;
+    private final long value;
     private final OffsetDateTime timestamp;
     private final Location location;
     private final float speed;
 
-    public static SensorTelemetry of(long id, Long value, OffsetDateTime timestamp, Location location, float speed) {
+    public static SensorTelemetry of(long id, long value, OffsetDateTime timestamp, Location location, float speed) {
         return new SensorTelemetry(id, value, timestamp, location, speed);
     }
 
-    public SensorTelemetry(long id, Long value, OffsetDateTime timestamp, Location location, float speed) {
+    public SensorTelemetry(long id, long value, OffsetDateTime timestamp, Location location, float speed) {
         this.id = id;
         this.value = value;
         this.timestamp = timestamp;
@@ -38,7 +38,7 @@ public class SensorTelemetry {
         return speed;
     }
 
-    public Long getValue() {
+    public long getValue() {
         return value;
     }
 }

+ 337 - 99
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java

@@ -2,6 +2,7 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.app.Application;
 import cz.senslog.telemetry.database.SortType;
+import cz.senslog.telemetry.database.domain.UnitLocation;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
 import cz.senslog.telemetry.utils.TernaryCondition;
 import io.vertx.core.http.HttpServerRequest;
@@ -16,15 +17,27 @@ import java.util.List;
 import java.util.function.*;
 import java.util.stream.Collectors;
 
+import static cz.senslog.telemetry.utils.FluentInvoke.of;
 import static java.lang.Boolean.parseBoolean;
 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 
 public class OpenAPIHandler {
 
+    private enum ResponseFormat {
+        JSON, GEOJSON
+        ;
+        public static ResponseFormat of(String format) {
+            return valueOf(format.toUpperCase());
+        }
+    }
+
     private static final int DEFAULT_MAX_DATA_LIMIT = 500;
     private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("UTC");
     private static final boolean DEFAULT_NAVIGATION_LINKS = true;
+    private static final ResponseFormat DEFAULT_RESPONSE_FORMAT = ResponseFormat.JSON;
 
     private static final BiFunction<OffsetDateTime, ZoneId, String> DATE_TIME_FORMATTER = (dateTime, zoneId) ->
             OffsetDateTime.ofInstant(dateTime.toInstant(), zoneId).format(ISO_OFFSET_DATE_TIME);
@@ -164,6 +177,12 @@ public class OpenAPIHandler {
             return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+           paramsJson.put("format", paramFormat.get(0));
+           return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -183,7 +202,8 @@ public class OpenAPIHandler {
             return String.format("%s/campaigns/%d/units/observations?offset=%d%s", host, campaignId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
         };
 
-        repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit)
+        switch (format) {
+            case JSON ->  repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit)
                 .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                         "next@NavigationLink", createNextNavLink.apply(paging.size())
                 ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -203,6 +223,34 @@ public class OpenAPIHandler {
                                         "observedValues", o.getObservedValues()
                                 )).collect(toList())))).encode()))
                 .onFailure(rc::fail);
+            case GEOJSON -> repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(JsonObject.of(
+                            "type", "FeatureCollection",
+                            "metadata", JsonObject.of(
+                                    "size", paging.size(),
+                                    "offset", offset,
+                                    "hasNext", paging.hasNext()
+                            ),
+                            "features", new JsonArray(
+                                    paging.data().stream().map(o -> JsonObject.of(
+                                            "type", "Feature",
+                                            "geometry", JsonObject.of(
+                                                    "type", "Point",
+                                                    "coordinates", JsonArray.of(
+                                                            o.getLocation().getLongitude(),
+                                                            o.getLocation().getLatitude(),
+                                                            o.getLocation().getAltitude()
+                                                    )
+                                            ),
+                                            "properties", JsonObject.of(
+                                                    "unitId", o.getUnitId(),
+                                                    "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                                    "speed", o.getSpeed(),
+                                                    "observations", o.getObservedValues()
+                                            )
+                                    )).collect(toList()))).encode()))
+                    .onFailure(rc::fail);
+            }
     }
 
     public void campaignIdUnitIdObservationsGET(RoutingContext rc) {
@@ -242,6 +290,12 @@ public class OpenAPIHandler {
            return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -261,25 +315,56 @@ public class OpenAPIHandler {
             return String.format("%s/campaigns/%d/units/%d/observations?offset=%d%s", host, campaignId, unitId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
         };
 
-        repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
-                .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
-                        "next@NavigationLink", createNextNavLink.apply(paging.size())
-                        ) : JsonObject.of()).mergeIn(JsonObject.of(
-                                "params", paramsJson,
-                                "size", paging.size(),
-                                "offset", offset,
-                                "hasNext", paging.hasNext(),
-                                "data", new JsonArray(
-                                        paging.data().stream().map(o -> JsonObject.of(
-                                                "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                                "speed", o.getSpeed(),
-                                                "location", JsonObject.of(
-                                                        "longitude", o.getLocation().getLongitude(),
-                                                        "latitude", o.getLocation().getLatitude(),
-                                                        "altitude", o.getLocation().getAltitude()),
-                                                "observedValues", o.getObservedValues()
-                                        )).collect(toList())))).encode()))
-                .onFailure(rc::fail);
+        switch (format) {
+            case JSON ->
+                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                            .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
+                                    "next@NavigationLink", createNextNavLink.apply(paging.size())
+                            ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                    "params", paramsJson,
+                                    "size", paging.size(),
+                                    "offset", offset,
+                                    "hasNext", paging.hasNext(),
+                                    "data", new JsonArray(
+                                            paging.data().stream().map(o -> JsonObject.of(
+                                                    "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                                    "speed", o.getSpeed(),
+                                                    "location", JsonObject.of(
+                                                            "longitude", o.getLocation().getLongitude(),
+                                                            "latitude", o.getLocation().getLatitude(),
+                                                            "altitude", o.getLocation().getAltitude()),
+                                                    "observedValues", o.getObservedValues()
+                                            )).collect(toList())))).encode()))
+                            .onFailure(rc::fail);
+            case GEOJSON ->
+                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                            .onSuccess(paging -> rc.response().end(JsonObject.of(
+                                    "type", "FeatureCollection",
+                                    "metadata", JsonObject.of(
+                                            "size", paging.size(),
+                                            "offset", offset,
+                                            "hasNext", paging.hasNext()
+                                    ),
+                                    "features", new JsonArray(
+                                            paging.data().stream().map(o -> JsonObject.of(
+                                                    "type", "Feature",
+                                                    "geometry", JsonObject.of(
+                                                            "type", "Point",
+                                                            "coordinates", JsonArray.of(
+                                                                    o.getLocation().getLongitude(),
+                                                                    o.getLocation().getLatitude(),
+                                                                    o.getLocation().getAltitude()
+                                                            )
+                                                    ),
+                                                    "properties", JsonObject.of(
+                                                            "unitId", o.getUnitId(),
+                                                            "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                                            "speed", o.getSpeed(),
+                                                            "observations", o.getObservedValues()
+                                                    )
+                                            )).collect(toList()))).encode()))
+                            .onFailure(rc::fail);
+        }
     }
 
     public void campaignIdUnitsObservationsLocationsGET(RoutingContext rc) {
@@ -317,6 +402,12 @@ public class OpenAPIHandler {
             return ZoneId.of(paramZone.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -327,20 +418,43 @@ public class OpenAPIHandler {
                 "Campaign@NavigationLink", String.format("%s/campaigns/%d",host, campaignId)
         ) : JsonObject.of();
 
-        repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
-                .onSuccess(locations -> rc.response().end(navLinks.mergeIn(JsonObject.of(
-                        "params", paramsJson,
-                        "size", locations.size(),
-                        "data", new JsonArray(locations.stream().map(l -> JsonObject.of(
-                            "unitId", l.getUnitId(),
-                            "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                            "location", JsonArray.of(
-                                        l.getLocation().getLongitude(),
-                                        l.getLocation().getLatitude(),
-                                        l.getLocation().getAltitude()
-                                )
-                        )).collect(toList())))).encode()))
-                .onFailure(rc::fail);
+        switch (format) {
+            case JSON ->  repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
+                    .onSuccess(locations -> rc.response().end(navLinks.mergeIn(JsonObject.of(
+                            "params", paramsJson,
+                            "size", locations.size(),
+                            "data", new JsonArray(locations.stream().map(l -> JsonObject.of(
+                                    "unitId", l.getUnitId(),
+                                    "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                    "location", JsonArray.of(
+                                            l.getLocation().getLongitude(),
+                                            l.getLocation().getLatitude(),
+                                            l.getLocation().getAltitude()
+                                    )
+                            )).collect(toList())))).encode()))
+                    .onFailure(rc::fail);
+            case GEOJSON -> repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
+                    .onSuccess(data -> of(data.stream().collect(groupingBy(UnitLocation::getUnitId))).then(unitLocation -> rc.response().end(JsonObject.of(
+                            "type", "FeatureCollection",
+                            "metadata", JsonObject.of(
+                                    "limitPerUnit", limitPerUnit
+                            ),
+                            "features", unitLocation.entrySet().stream().map(entry -> JsonObject.of(
+                                    "type", "Feature",
+                                    "geometry", JsonObject.of(
+                                            "type", "MultiPoint",
+                                            "properties", JsonObject.of(
+                                                    "unitId", entry.getKey()
+                                            ),
+                                            "coordinates", new JsonArray(entry.getValue().stream().map(l -> JsonArray.of(
+                                                    l.getLocation().getLongitude(),
+                                                    l.getLocation().getLatitude(),
+                                                    l.getLocation().getAltitude()
+                                            )).collect(toList()))
+                                    )
+                            )).collect(toList())).encode())))
+                    .onFailure(rc::fail);
+        }
     }
 
     public void campaignIdUnitIdLocationsGET(RoutingContext rc) {
@@ -380,7 +494,18 @@ public class OpenAPIHandler {
             return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> filter = rc.queryParam("filter");
+        // TODO implement filter
+        /*
+            filtr=sensorId(10003)gt10
+            filtr=driverId(34245)eq3
+         */
 
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
@@ -402,8 +527,8 @@ public class OpenAPIHandler {
             return String.format("%s/campaigns/%d/units/%d/observations/locations?offset=%d&%s", host, campaignId, unitId, newOffset, urlParams);
         };
 
-        if (filter.isEmpty()) {
-            repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+        switch (format) {
+            case JSON ->  repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
                     .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                             "next@NavigationLink", createNextNavLink.apply(paging.size())
                     ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -421,12 +546,26 @@ public class OpenAPIHandler {
                                             )
                                     )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-        } else {
-            // TODO implement filter
-            /*
-            	filtr=sensorId(10003)gt10
-            	filtr=driverId(34245)eq3
-             */
+            case GEOJSON -> repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(JsonObject.of(
+                                "type", "Feature",
+                                "metadata", JsonObject.of(
+                                        "size", paging.size(),
+                                        "offset", offset,
+                                        "hasNext", paging.hasNext()
+                                ),
+                                "properties", JsonObject.of(
+                                        "unitId", unitId
+                                ),
+                                "geometry", JsonObject.of(
+                                        "type", "MultiPoint",
+                                        "coordinates", new JsonArray(paging.data().stream().map(l -> JsonArray.of(
+                                                l.getLocation().getLongitude(),
+                                                l.getLocation().getLatitude(),
+                                                l.getLocation().getAltitude()
+                                        )).collect(toList())))
+                            ).encode()))
+                    .onFailure(rc::fail);
         }
     }
 
@@ -753,6 +892,12 @@ public class OpenAPIHandler {
             return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -772,25 +917,56 @@ public class OpenAPIHandler {
             return String.format("%s/campaigns/%d/units/%d/sensors/%d/observations?offset=%d%s", host, campaignId, unitId, sensorId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
         };
 
-        repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit)
-                .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
-                        "next@NavigationLink", createNextNavLink.apply(paging.size())
-                ) : JsonObject.of()).mergeIn(JsonObject.of(
-                        "params", paramsJson,
-                        "size", paging.size(),
-                        "offset", offset,
-                        "hasNext", paging.hasNext(),
-                        "data", new JsonArray(
-                                paging.data().stream().map(o -> JsonObject.of(
-                                        "value", o.getValue(),
-                                        "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                        "speed", o.getSpeed(),
-                                        "location", JsonObject.of(
-                                                "longitude", o.getLocation().getLongitude(),
-                                                "latitude", o.getLocation().getLatitude(),
-                                                "altitude", o.getLocation().getAltitude())
-                                )).collect(toList())))).encode()))
-                .onFailure(rc::fail);
+        switch (format) {
+            case JSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
+                            "next@NavigationLink", createNextNavLink.apply(paging.size())
+                    ) : JsonObject.of()).mergeIn(JsonObject.of(
+                            "params", paramsJson,
+                            "size", paging.size(),
+                            "offset", offset,
+                            "hasNext", paging.hasNext(),
+                            "data", new JsonArray(
+                                    paging.data().stream().map(o -> JsonObject.of(
+                                            "value", o.getValue(),
+                                            "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                            "speed", o.getSpeed(),
+                                            "location", JsonObject.of(
+                                                    "longitude", o.getLocation().getLongitude(),
+                                                    "latitude", o.getLocation().getLatitude(),
+                                                    "altitude", o.getLocation().getAltitude())
+                                    )).collect(toList())))).encode()))
+                    .onFailure(rc::fail);
+            case GEOJSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(JsonObject.of(
+                            "type", "FeatureCollection",
+                            "metadata", JsonObject.of(
+                                    "size", paging.size(),
+                                    "offset", offset,
+                                    "hasNext", paging.hasNext()
+                            ),
+                            "features", new JsonArray(
+                                    paging.data().stream().map(o -> JsonObject.of(
+                                            "type", "Feature",
+                                            "geometry", JsonObject.of(
+                                                    "type", "Point",
+                                                    "coordinates", JsonArray.of(
+                                                            o.getLocation().getLongitude(),
+                                                            o.getLocation().getLatitude(),
+                                                            o.getLocation().getAltitude()
+                                                    )
+                                            ),
+                                            "properties", JsonObject.of(
+                                                    "unitId", unitId,
+                                                    "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                                    "speed", o.getSpeed(),
+                                                    "observations", JsonObject.of(
+                                                            String.valueOf(sensorId), o.getValue()
+                                                    )
+                                            )
+                                    )).collect(toList()))).encode()))
+                    .onFailure(rc::fail);
+        }
     }
 
     public void driversGET(RoutingContext rc) {
@@ -975,7 +1151,7 @@ public class OpenAPIHandler {
                                 "self@NavigationLink", String.format("%s/drivers/%d/actions/%d/units/%d", host, driverId, actionId, u.getUnitId()),
                                 "Unit@NavigationLink", String.format("%s/units/%d", host, u.getUnitId()),
                                 "DriverAction@NavigationLink", String.format("%s/drivers/%d/actions/%d", host, driverId, actionId),
-                                "Events@NavigationLink", String.format("%s/ drivers/%d/units/%d/actions/%d/events", host, driverId, u.getUnitId(), actionId)
+                                "Events@NavigationLink", String.format("%s/drivers/%d/units/%d/actions/%d/events", host, driverId, u.getUnitId(), actionId)
                         ) : JsonObject.of()).mergeIn(JsonObject.of(
                                 "unitId", u.getUnitId(),
                                 "name", u.getName(),
@@ -1099,6 +1275,12 @@ public class OpenAPIHandler {
             return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -1118,25 +1300,54 @@ public class OpenAPIHandler {
             return String.format("%s/events/%d/observations?offset=%d%s", host, eventId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
         };
 
-        repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
-                .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
-                        "next@NavigationLink", createNextNavLink.apply(paging.size())
-                ) : JsonObject.of()).mergeIn(JsonObject.of(
-                        "params", paramsJson,
-                        "size", paging.size(),
-                        "offset", offset,
-                        "hasNext", paging.hasNext(),
-                        "data", new JsonArray(
-                                paging.data().stream().map(o -> JsonObject.of(
-                                        "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                        "speed", o.getSpeed(),
-                                        "location", JsonObject.of(
-                                                "longitude", o.getLocation().getLongitude(),
-                                                "latitude", o.getLocation().getLatitude(),
-                                                "altitude", o.getLocation().getAltitude()),
-                                        "observedValues", o.getObservedValues()
-                                )).collect(toList())))).encode()))
-                .onFailure(rc::fail);
+        switch (format) {
+            case JSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
+                            "next@NavigationLink", createNextNavLink.apply(paging.size())
+                    ) : JsonObject.of()).mergeIn(JsonObject.of(
+                            "params", paramsJson,
+                            "size", paging.size(),
+                            "offset", offset,
+                            "hasNext", paging.hasNext(),
+                            "data", new JsonArray(
+                                    paging.data().stream().map(o -> JsonObject.of(
+                                            "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                            "speed", o.getSpeed(),
+                                            "location", JsonObject.of(
+                                                    "longitude", o.getLocation().getLongitude(),
+                                                    "latitude", o.getLocation().getLatitude(),
+                                                    "altitude", o.getLocation().getAltitude()),
+                                            "observedValues", o.getObservedValues()
+                                    )).collect(toList())))).encode()))
+                    .onFailure(rc::fail);
+            case GEOJSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(JsonObject.of(
+                            "type", "FeatureCollection",
+                            "metadata", JsonObject.of(
+                                    "size", paging.size(),
+                                    "offset", offset,
+                                    "hasNext", paging.hasNext()
+                            ),
+                            "features", new JsonArray(
+                                    paging.data().stream().map(o -> JsonObject.of(
+                                            "type", "Feature",
+                                            "geometry", JsonObject.of(
+                                                    "type", "Point",
+                                                    "coordinates", JsonArray.of(
+                                                            o.getLocation().getLongitude(),
+                                                            o.getLocation().getLatitude(),
+                                                            o.getLocation().getAltitude()
+                                                    )
+                                            ),
+                                            "properties", JsonObject.of(
+                                                    "unitId", o.getUnitId(),
+                                                    "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                                    "speed", o.getSpeed(),
+                                                    "observations", o.getObservedValues()
+                                            )
+                                    )).collect(toList()))).encode()))
+                    .onFailure(rc::fail);
+        }
     }
 
     public void eventIdLocationsGET(RoutingContext rc) {
@@ -1175,6 +1386,12 @@ public class OpenAPIHandler {
             return Integer.parseInt(paramLimit.get(0));
         });
 
+        List<String> paramFormat = rc.queryParam("format");
+        ResponseFormat format = TernaryCondition.<ResponseFormat>ternaryIf(paramFormat::isEmpty, DEFAULT_RESPONSE_FORMAT, () -> {
+            paramsJson.put("format", paramFormat.get(0));
+            return ResponseFormat.of(paramFormat.get(0));
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -1194,24 +1411,45 @@ public class OpenAPIHandler {
             return String.format("%s/events/%d/observations/locations?offset=%d%s", host, eventId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
         };
 
-        repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
-                .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
-                        "next@NavigationLink", createNextNavLink.apply(paging.size())
-                ) : JsonObject.of()).mergeIn(JsonObject.of(
-                        "params", paramsJson,
-                        "size", paging.size(),
-                        "offset", offset,
-                        "hasNext", paging.hasNext(),
-                        "data", new JsonArray(
-                                paging.data().stream().map(l -> JsonObject.of(
-                                        "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                        "location", JsonArray.of(
-                                                l.getLocation().getLongitude(),
-                                                l.getLocation().getLatitude(),
-                                                l.getLocation().getAltitude()
-                                        )
-                                )).collect(toList())))).encode()))
-                .onFailure(rc::fail);
+        switch (format) {
+            case JSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
+                            "next@NavigationLink", createNextNavLink.apply(paging.size())
+                    ) : JsonObject.of()).mergeIn(JsonObject.of(
+                            "params", paramsJson,
+                            "size", paging.size(),
+                            "offset", offset,
+                            "hasNext", paging.hasNext(),
+                            "data", new JsonArray(
+                                    paging.data().stream().map(l -> JsonObject.of(
+                                            "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                            "location", JsonArray.of(
+                                                    l.getLocation().getLongitude(),
+                                                    l.getLocation().getLatitude(),
+                                                    l.getLocation().getAltitude()
+                                            )
+                                    )).collect(toList())))).encode()))
+                    .onFailure(rc::fail);
+            case GEOJSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(JsonObject.of(
+                            "type", "Feature",
+                            "metadata", JsonObject.of(
+                                    "size", paging.size(),
+                                    "offset", offset,
+                                    "hasNext", paging.hasNext()
+                            ),
+                            "properties", ofNullable(paging.size() > 0  ? paging.data().get(0) : null)
+                                    .map(l -> JsonObject.of("unitId", l.getUnitId())).orElseGet(JsonObject::new),
+                            "geometry", JsonObject.of(
+                                    "type", "MultiPoint",
+                                    "coordinates", new JsonArray(paging.data().stream().map(l -> JsonArray.of(
+                                            l.getLocation().getLongitude(),
+                                            l.getLocation().getLatitude(),
+                                            l.getLocation().getAltitude()
+                                    )).collect(toList())))
+                    ).encode()))
+                    .onFailure(rc::fail);
+        }
     }
 
     public void unitIdDriversGET(RoutingContext rc) {

+ 20 - 0
src/main/java/cz/senslog/telemetry/utils/FluentInvoke.java

@@ -0,0 +1,20 @@
+package cz.senslog.telemetry.utils;
+
+import java.util.function.Function;
+
+public final class FluentInvoke<T> {
+
+    private final T object;
+
+    public static <T> FluentInvoke<T> of(T object) {
+        return new FluentInvoke<>(object);
+    }
+
+    public FluentInvoke(T object) {
+        this.object = object;
+    }
+
+    public <D> FluentInvoke<D> then(Function<T, D> fnc) {
+        return of(fnc.apply(object));
+    }
+}

+ 15 - 0
src/main/resources/openAPISpec.yaml

@@ -105,6 +105,7 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/navigationLinksParam'
+        - $ref: '#/components/parameters/formatParam'
       responses:
         200:
           description: JSON containing stream of telemetry data
@@ -130,6 +131,7 @@ paths:
         - $ref: '#/components/parameters/toParam'
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/sortParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -180,6 +182,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -208,6 +211,7 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/filterParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -281,6 +285,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -829,6 +834,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -855,6 +861,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
+        - $ref: '#/components/parameters/formatParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -1020,6 +1027,14 @@ components:
         Value:
           value: driverId(34245)eq3
           summary: Filter locations of units driven by Driver ID 34245 == Activity 3
+    formatParam:
+      in: query
+      name: format
+      schema:
+        type: string
+        enum: [json, geojson]
+        default: json
+      required: false
 
   schemas:
     CampaignBasicInfo: