Quellcode durchsuchen

Refactored code, Edited API, Implemented APIs with ID [campaignIdUnitsObservationsLocationsGET, campaignIdUnitsObservationsGET, campaignIdUnitsGET, campaignIdGET, campaignsGET]

Lukas Cerny vor 1 Jahr
Ursprung
Commit
8ef35f8145

+ 4 - 1
README.md

@@ -12,4 +12,7 @@ docker run --name senslog_telemetry \
     --detach senslog/telemetry
 
 
-docker run --name senslog_telemetry --env-file ./production.env -v /home/lcerny/logs:/volume/logs -p 8081:8080 -p 8083:9999/tcp -d senslog/telemetry
+docker run --name senslog_telemetry --env-file ./production.env -v /home/lcerny/logs:/volume/logs -p 8081:8080 -p 8083:9999/tcp -d senslog/telemetry
+
+
+docker compose up --no-deps --build telemetry-dev  

+ 3 - 3
src/main/java/cz/senslog/telemetry/database/domain/CampaignUnit.java

@@ -5,9 +5,9 @@ import java.time.OffsetDateTime;
 
 public class CampaignUnit {
 
-    private long unitId;
-    private String description;
-    private OffsetDateTime fromTime, toTime;
+    private final long unitId;
+    private final String description;
+    private final OffsetDateTime fromTime, toTime;
 
 
     public static CampaignUnit of(long unitId, String description, OffsetDateTime fromTime, OffsetDateTime toTime) {

+ 10 - 0
src/main/java/cz/senslog/telemetry/database/domain/Location.java

@@ -41,4 +41,14 @@ public class Location {
     public float getAngle() {
         return angle;
     }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"longitude\":" + longitude +
+                ", \"latitude:\"" + latitude +
+                ", \"altitude:\"" + altitude +
+                ", \"angle:\"" + angle +
+                '}';
+    }
 }

+ 0 - 120
src/main/java/cz/senslog/telemetry/database/domain/ObsTelemetry.java

@@ -1,120 +0,0 @@
-package cz.senslog.telemetry.database.domain;
-
-import io.vertx.core.json.JsonObject;
-
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-
-public class ObsTelemetry {
-
-    private long id;
-    private OffsetDateTime timestamp;
-    private long unitId;
-    private JsonObject observedValues;
-    private float longitude;
-    private float latitude;
-    private float altitude;
-    private float angle;
-    private float speed;
-
-    public static ObsTelemetry of(long id, OffsetDateTime timestamp, long unitId, float speed, Location location, JsonObject observedValues) {
-        ObsTelemetry o = new ObsTelemetry();
-        o.setId(id);
-        o.setTimestamp(timestamp);
-        o.setUnitId(unitId);
-        o.setSpeed(speed);
-        o.setObservedValues(observedValues);
-        o.setLongitude(location.getLongitude());
-        o.setLatitude(location.getLatitude());
-        o.setAltitude(location.getAltitude());
-        o.setAngle(location.getAngle());
-        return o;
-    }
-
-    public long getId() {
-        return id;
-    }
-
-    public void setId(long id) {
-        this.id = id;
-    }
-
-    public OffsetDateTime getTimestamp() {
-        return timestamp;
-    }
-
-    public void setTimestamp(OffsetDateTime timestamp) {
-        this.timestamp = timestamp;
-    }
-
-    public long getUnitId() {
-        return unitId;
-    }
-
-    public void setUnitId(long unitId) {
-        this.unitId = unitId;
-    }
-
-    public JsonObject getObservedValues() {
-        return observedValues;
-    }
-
-    public void setObservedValues(JsonObject observedValues) {
-        this.observedValues = observedValues;
-    }
-
-    public float getLongitude() {
-        return longitude;
-    }
-
-    public void setLongitude(float longitude) {
-        this.longitude = longitude;
-    }
-
-    public float getLatitude() {
-        return latitude;
-    }
-
-    public void setLatitude(float latitude) {
-        this.latitude = latitude;
-    }
-
-    public float getAltitude() {
-        return altitude;
-    }
-
-    public void setAltitude(float altitude) {
-        this.altitude = altitude;
-    }
-
-    public float getAngle() {
-        return angle;
-    }
-
-    public void setAngle(float angle) {
-        this.angle = angle;
-    }
-
-    public float getSpeed() {
-        return speed;
-    }
-
-    public void setSpeed(float speed) {
-        this.speed = speed;
-    }
-
-    @Override
-    public String toString() {
-        return "{" +
-                "\"id\":" + id +
-                ", \"timestamp\":" + "\""+timestamp.format(DateTimeFormatter.ISO_DATE_TIME)+"\"" +
-                ", \"unitId\":" + unitId +
-                ", \"observedValues\":" + observedValues +
-                ", \"longitude\":" + longitude +
-                ", \"latitude\":" + latitude +
-                ", \"altitude\":" + altitude +
-                ", \"angle\":" + angle +
-                ", \"speed\":" + speed +
-                '}';
-    }
-}

+ 2 - 0
src/main/java/cz/senslog/telemetry/database/domain/Sensor.java

@@ -27,4 +27,6 @@ public class Sensor {
     public int getIoID() {
         return ioID;
     }
+
+
 }

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

@@ -6,13 +6,13 @@ public class UnitLocation {
 
     private final long unitId;
     private final OffsetDateTime timestamp;
-    private final float [] location;
+    private final Location location;
 
-    public static UnitLocation of(long unitId, OffsetDateTime timestamp, float[] location) {
+    public static UnitLocation of(long unitId, OffsetDateTime timestamp, Location location) {
         return new UnitLocation(unitId, timestamp, location);
     }
 
-    private UnitLocation(long unitId, OffsetDateTime timestamp, float[] location) {
+    private UnitLocation(long unitId, OffsetDateTime timestamp, Location location) {
         this.unitId = unitId;
         this.timestamp = timestamp;
         this.location = location;
@@ -26,7 +26,7 @@ public class UnitLocation {
         return timestamp;
     }
 
-    public float[] getLocation() {
+    public Location getLocation() {
         return location;
     }
 }

+ 69 - 0
src/main/java/cz/senslog/telemetry/database/domain/UnitTelemetry.java

@@ -0,0 +1,69 @@
+package cz.senslog.telemetry.database.domain;
+
+import io.vertx.core.json.JsonObject;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class UnitTelemetry {
+
+    private final long id;
+    private final long unitId;
+    private final OffsetDateTime timestamp;
+    private final Location location;
+    private final float speed;
+    private final JsonObject observedValues;
+
+    public static UnitTelemetry of(long id, long unitId, OffsetDateTime timestamp, Location location, float speed, JsonObject observedValues) {
+        return new UnitTelemetry(id, unitId, timestamp, location, speed, observedValues);
+    }
+
+    public static UnitTelemetry of(long unitId, OffsetDateTime timestamp, Location location, float speed, JsonObject observedValues) {
+        return of(-1, unitId, timestamp, location, speed, observedValues);
+    }
+
+    private UnitTelemetry(long id, long unitId, OffsetDateTime timestamp, Location location, float speed, JsonObject observedValues) {
+        this.id = id;
+        this.unitId = unitId;
+        this.timestamp = timestamp;
+        this.location = location;
+        this.speed = speed;
+        this.observedValues = observedValues;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public Location getLocation() {
+        return location;
+    }
+
+    public float getSpeed() {
+        return speed;
+    }
+
+    public JsonObject getObservedValues() {
+        return observedValues;
+    }
+
+    @Override
+    public String toString() {
+        return "{" +
+                "\"id\":" + id +
+                ", \"timestamp\":" + "\"" + timestamp.format(DateTimeFormatter.ISO_DATE_TIME) + "\"" +
+                ", \"unitId\":" + unitId +
+                ", \"observedValues\":" + observedValues +
+                ", \"location\":" + location +
+                ", \"speed\":" + speed +
+                '}';
+    }
+}

+ 94 - 56
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -18,7 +18,6 @@ import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -60,17 +59,18 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<Integer> saveTelemetry(ObsTelemetry data) {
-        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom, speed) " +
-                        "VALUES ($1, $2, $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6), 4326), $7) RETURNING (obs_id)")
+    public Future<Integer> saveTelemetry(UnitTelemetry data) {
+        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(unit_id, time_stamp, the_geom, speed, observed_values) " +
+                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::json) RETURNING (obs_id)")
                 .execute(Tuple.of(
-                        data.getTimestamp(),
                         data.getUnitId(),
-                        data.getObservedValues().toString(),
-                        data.getLongitude(),
-                        data.getLatitude(),
-                        data.getAltitude(),
-                        data.getSpeed()
+                        data.getTimestamp(),
+                        data.getLocation().getLongitude(),
+                        data.getLocation().getLatitude(),
+                        data.getLocation().getAltitude(),
+                        data.getLocation().getAngle(),
+                        data.getSpeed(),
+                        data.getObservedValues().toString()
                 ))
                 .onComplete(res -> {
                     if (res.succeeded()) {
@@ -84,18 +84,19 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<Integer> saveAllTelemetry(List<ObsTelemetry> data) {
+    public Future<Integer> saveAllTelemetry(List<UnitTelemetry> data) {
         List<Tuple> tuples = data.stream().map(d -> Tuple.of(
-                d.getTimestamp(),
                 d.getUnitId(),
-                d.getObservedValues().toString(),
-                d.getLongitude(),
-                d.getLatitude(),
-                d.getAltitude(),
-                d.getSpeed()
+                d.getTimestamp(),
+                d.getLocation().getLongitude(),
+                d.getLocation().getLatitude(),
+                d.getLocation().getAltitude(),
+                d.getLocation().getAngle(),
+                d.getSpeed(),
+                d.getObservedValues().toString()
         )).collect(toList());
-        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom, speed) " +
-                        "VALUES ($1, $2,  $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6), 4326), $7)")
+        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(unit_id, time_stamp, the_geom, speed, observed_values) " +
+                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::json)")
                 .executeBatch(tuples)
                 .map(SqlResult::rowCount);
     }
@@ -225,39 +226,72 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId) {
-        return client.preparedQuery(
-                    "SELECT obs.obs_id, obs.time_stamp, obs.unit_id, obs.observed_values, obs.speed " +
-                            "ST_X (ST_Transform (obs.the_geom, 4326)) AS long, " +
-                            "ST_Y (ST_Transform (obs.the_geom, 4326)) AS lat " +
-                            "ST_Z (ST_Transform (obs.the_geom, 4326)) AS alt " +
-                        "FROM maplog.obs_telemetry AS obs " +
-                        "JOIN maplog.unit AS u ON u.unit_id = obs.unit_id " +
-                        "JOIN maplog.unit_to_campaign AS u_cam ON u_cam.unit_id = u.unit_id " +
-                        "JOIN maplog.campaign AS c ON c.campaign_id = u_cam.camp_id " +
-                        "WHERE c.campaign_id = $1 " +
-                            "AND c.from_time <= u_cam.from_time " +
-                            "AND c.to_time >= u_cam.to_time"
-                )
-                .execute(Tuple.of(campaignId))
+    public Future<List<UnitTelemetry>> findObservationsByCampaignId(
+            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
+        String whereTimestampClause;
+        Tuple tupleParams;
+        if (from != null && to != null) {
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $5 > c.from_time THEN c.from_time ELSE $5 END) AND tel.time_stamp <= (CASE WHEN $6 > c.to_time THEN c.to_time ELSE $6 END)";
+            tupleParams = Tuple.of(campaignId, offset, limit, zone.getId(), from, to);
+        } else if (from != null) {
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $5 > c.from_time THEN c.from_time ELSE $5 END) AND tel.time_stamp <= c.to_time";
+            tupleParams = Tuple.of(campaignId, offset, limit, zone.getId(), from);
+        } else if (to != null) {
+            whereTimestampClause = "tel.time_stamp >= c.from_time AND tel.time_stamp <= (CASE WHEN $5 > c.to_time THEN c.to_time ELSE $5 END)";
+            tupleParams = Tuple.of(campaignId, offset, limit, zone.getId(), to);
+        } else {
+            whereTimestampClause = "tel.time_stamp >= c.from_time AND tel.time_stamp <= c.to_time";
+            tupleParams = Tuple.of(campaignId, offset, limit, zone.getId());
+        }
+
+        String sql = "SELECT tel.obs_id, tel.unit_id, tel.observed_values::json, tel.speed, " +
+                "tel.time_stamp, $4 AS zone_id, " + // ::timestamp with time zone at time zone $4 AS time_stamp
+                "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
+                "ST_Y (ST_Transform (tel.the_geom, 4326)) AS lat, " +
+                "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt, " +
+                "ST_M (tel.the_geom) AS angle " +
+                "FROM maplog.obs_telemetry AS tel " +
+                "JOIN maplog.unit_to_campaign AS utc ON tel.unit_id = utc.unit_id " +
+                "JOIN maplog.campaign AS c ON c.campaign_id = utc.camp_id " +
+                "WHERE utc.camp_id = $1 AND " + whereTimestampClause + " " +
+                "ORDER BY tel.time_stamp OFFSET $2 LIMIT $3;";
+
+        return client.preparedQuery(sql)
+                .execute(tupleParams)
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false)
-                        .map(r -> ObsTelemetry.of(
+                        .map(r -> UnitTelemetry.of(
                                 r.getLong("obs_id"),
-                                r.getOffsetDateTime("time_stamp"),
                                 r.getLong("unit_id"),
-                                r.getFloat("speed"),
+                                r.getOffsetDateTime("time_stamp"),
                                 Location.of(
                                         r.getFloat("long"),
                                         r.getFloat("lat"),
-                                        r.getFloat("alt")),
-                                r.getJsonObject("observed_values")
+                                        r.getFloat("alt"),
+                                        r.getFloat("angle")),
+                                r.getFloat("speed"),
+                                new JsonObject(r.getString("observed_values"))
                         ))
-                        .collect(Collectors.toList())
-                );
+                        .collect(Collectors.toList()))
+                .onFailure(logger::catching);
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(
+            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
+        return findObservationsByCampaignId(campaignId, from, to, zone, offset, limit+1)
+                .map(data -> {
+                    boolean hasNext = data.size() > limit;
+                    if (hasNext) {
+                        data.remove(data.size() - 1);
+                    }
+                    return new PagingRetrieve<>(hasNext, data.size(), data);
+                });
     }
 
     @Override
-    public Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(
+    public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(
             long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
     ) {
         String whereTimestampClause;
@@ -276,11 +310,12 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId());
         }
 
-        String sql = "SELECT tel.obs_id, tel.unit_id, tel.observed_values::json, tel.speed " +
+        String sql = "SELECT tel.obs_id, tel.unit_id, tel.observed_values::json, tel.speed, " +
                     "tel.time_stamp, $5 AS zone_id, " + // ::timestamp with time zone at time zone $5 AS time_stamp
                     "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
                     "ST_Y (ST_Transform (tel.the_geom, 4326)) AS lat, " +
-                    "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt " +
+                    "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt, " +
+                    "ST_M (tel.the_geom) AS angle " +
                 "FROM maplog.obs_telemetry AS tel " +
                 "JOIN maplog.unit u on u.unit_id = tel.unit_id " +
                 "JOIN maplog.unit_to_campaign utc on tel.unit_id = utc.unit_id " +
@@ -290,22 +325,23 @@ public class MapLogRepository implements SensLogRepository {
         return client.preparedQuery(sql)
                 .execute(tupleParams)
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false)
-                        .map(r -> ObsTelemetry.of(
+                        .map(r -> UnitTelemetry.of(
                                 r.getLong("obs_id"),
-                                r.getOffsetDateTime("time_stamp"),
                                 r.getLong("unit_id"),
-                                r.getFloat("speed"),
+                                r.getOffsetDateTime("time_stamp"),
                                 Location.of(
                                         r.getFloat("long"),
                                         r.getFloat("lat"),
-                                        r.getFloat("alt")),
+                                        r.getFloat("alt"),
+                                        r.getFloat("angle")),
+                                r.getFloat("speed"),
                                 new JsonObject(r.getString("observed_values"))))
                         .collect(Collectors.toList()))
                 .onFailure(logger::catching);
     }
 
     @Override
-    public Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(
             long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
     ) {
         return findObservationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1)
@@ -354,12 +390,12 @@ public class MapLogRepository implements SensLogRepository {
                         .map(r -> UnitLocation.of(
                                 r.getLong("unit_id"),
                                 r.getOffsetDateTime("time_stamp"),
-                                new float[] {
+                                Location.of(
                                         r.getFloat("long"),
                                         r.getFloat("lat"),
                                         r.getFloat("alt")
-                                })
-                        )
+                                )
+                        ))
                         .collect(Collectors.toList()))
                 .onFailure(logger::catching);
     }
@@ -401,7 +437,8 @@ public class MapLogRepository implements SensLogRepository {
         String sql = "SELECT unit_id, time_stamp, $3 AS zone_id, " + // ::timestamp with time zone at time zone $5 AS time_stamp
                     "ST_X (ST_Transform (the_geom, 4326)) AS long, " +
                     "ST_Y (ST_Transform (the_geom, 4326)) AS lat, " +
-                    "ST_Z (ST_Transform (the_geom, 4326)) AS alt " +
+                    "ST_Z (ST_Transform (the_geom, 4326)) AS alt, " +
+                    "ST_M (the_geom) AS angle " +
                 "FROM (SELECT *, row_number() OVER (PARTITION BY unit_id ORDER BY time_stamp "+ sort.name() +" ) AS rn " +
                 "FROM (SELECT obs.* FROM maplog.obs_telemetry obs JOIN maplog.unit_to_campaign utc ON obs.unit_id = utc.unit_id "+whereTimestampClause+") AS data) AS g " +
                 "WHERE rn <= $2";
@@ -412,11 +449,12 @@ public class MapLogRepository implements SensLogRepository {
                         .map(r -> UnitLocation.of(
                                 r.getLong("unit_id"),
                                 r.getOffsetDateTime("time_stamp"),
-                                new float[] {
+                                Location.of(
                                         r.getFloat("long"),
                                         r.getFloat("lat"),
-                                        r.getFloat("alt")
-                                }
+                                        r.getFloat("alt"),
+                                        r.getFloat("angle")
+                                )
                         ))
                         .collect(toList()))
                 .onFailure(logger::catching);

+ 12 - 7
src/main/java/cz/senslog/telemetry/database/repository/MockMapLogRepository.java

@@ -22,13 +22,13 @@ public class MockMapLogRepository implements SensLogRepository {
         return Future.succeededFuture(1);
     }
 
-    public Future<Integer> saveTelemetry(ObsTelemetry data) {
+    public Future<Integer> saveTelemetry(UnitTelemetry data) {
         logger.info("SAVE: timestamp {} | unit_id {} | observed_value {} | lon {} | lat {}",
-                data.getTimestamp(), data.getUnitId(), data.getObservedValues().toString(), data.getLongitude(), data.getLatitude());
+                data.getTimestamp(), data.getUnitId(), data.getObservedValues().toString(), data.getLocation().getLongitude(), data.getLocation().getLatitude());
         return Future.succeededFuture(1);
     }
 
-    public Future<Integer> saveAllTelemetry(List<ObsTelemetry> data) {
+    public Future<Integer> saveAllTelemetry(List<UnitTelemetry> data) {
         data.forEach(this::saveTelemetry);
         return Future.succeededFuture(data.size());
     }
@@ -65,18 +65,23 @@ public class MockMapLogRepository implements SensLogRepository {
         return Future.succeededFuture(List.of(1L));
     }
 
-
-    public Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId) {
+    @Override
+    public Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+        return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
+    }
+
+    @Override
+    public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 

+ 11 - 5
src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java

@@ -13,8 +13,8 @@ import java.util.Map;
 public interface SensLogRepository {
 
     Future<Integer> updateDriverAction(DriverAction data);
-    Future<Integer> saveTelemetry(ObsTelemetry data);
-    Future<Integer> saveAllTelemetry(List<ObsTelemetry> data);
+    Future<Integer> saveTelemetry(UnitTelemetry data);
+    Future<Integer> saveAllTelemetry(List<UnitTelemetry> data);
 
     Future<Boolean> createSensor(Sensor sensor, long unitId);
 
@@ -27,11 +27,17 @@ public interface SensLogRepository {
     Future<List<Campaign>> allCampaigns();
     Future<Campaign> findCampaignById(long campaignId);
     Future<List<Long>> findUnitIdsByCampaignId(long campaignId);
-    Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId);
-    Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
-    Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+
+    Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+
+
+    Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+
     Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
     Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+
     Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId);
     Future<List<Sensor>> findSensorsByUnitId(long unitId);
     Future<List<CampaignUnit>> findUnitsByCampaignId(long campaignId);

+ 6 - 16
src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java

@@ -1,9 +1,6 @@
 package cz.senslog.telemetry.server;
 
-import cz.senslog.telemetry.database.domain.DriverAction;
-import cz.senslog.telemetry.database.domain.ObsTelemetry;
-import cz.senslog.telemetry.database.domain.Sensor;
-import cz.senslog.telemetry.database.domain.Unit;
+import cz.senslog.telemetry.database.domain.*;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
 import cz.senslog.telemetry.protocol.domain.*;
 import cz.senslog.telemetry.protocol.Fm4ex;
@@ -17,6 +14,7 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.time.Instant;
+import java.time.OffsetDateTime;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
@@ -160,18 +158,10 @@ public class Fm4exSocketHandler {
 
             long unitId = contextUnit.getUnitId();
             return repo.findSensorsByUnitIdGroupById(unitId).map(ioToSensor -> {
-                List<ObsTelemetry> telemetries = new ArrayList<>(avlRecords.length);
+                List<UnitTelemetry> telemetries = new ArrayList<>(avlRecords.length);
                 for (AVLTelemetryObservation avl : avlRecords) {
-                    ObsTelemetry obs = new ObsTelemetry();
-                    telemetries.add(obs);
-
-                    obs.setUnitId(unitId);
-                    obs.setTimestamp(avl.getTimestamp().atZone(UTC).toOffsetDateTime());
-                    obs.setLatitude(avl.getLatitude());
-                    obs.setLongitude(avl.getLongitude());
-                    obs.setAltitude(avl.getAltitude());
-                    obs.setSpeed(avl.getSpeed());
-                    obs.setAngle(avl.getAngle());
+                    Location location = Location.of(avl.getLongitude(), avl.getLatitude(), avl.getAltitude(), avl.getAngle());
+                    OffsetDateTime timestamp = avl.getTimestamp().atZone(UTC).toOffsetDateTime();
                     JsonObject observedValues = new JsonObject();
                     for (IOProperty io : avl.getIoProperties()) {
                         Sensor sensor = ioToSensor.get((long)io.getId());
@@ -181,7 +171,7 @@ public class Fm4exSocketHandler {
                             logger.error("Sensor for the IO Property <{}> does not exist.", io.getId());
                         }
                     }
-                    obs.setObservedValues(observedValues);
+                    telemetries.add(UnitTelemetry.of(unitId, timestamp, location, avl.getSpeed(), observedValues));
                 }
                 return telemetries;
             }).flatMap(telemetries -> repo.saveAllTelemetry(telemetries)

+ 5 - 6
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -9,11 +9,9 @@ import io.vertx.core.Handler;
 import io.vertx.core.Promise;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.json.JsonObject;
-import io.vertx.ext.web.Route;
 import io.vertx.ext.web.Router;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.handler.CorsHandler;
-import io.vertx.ext.web.handler.LoggerHandler;
 import io.vertx.ext.web.openapi.RouterBuilder;
 import io.vertx.pgclient.PgPool;
 import io.vertx.sqlclient.RowSet;
@@ -55,12 +53,13 @@ public final class HttpVertxServer extends AbstractVerticle {
                     OpenAPIHandler apiHandler = OpenAPIHandler.create(repo);
 
                     openAPIRouterBuilder.operation("infoGET").handler(apiHandler::info);
-//                    openAPIRouterBuilder.operation("campaignAllGET").handler(apiHandler::campaignAll);
-//                    openAPIRouterBuilder.operation("campaignIdGET").handler(apiHandler::campaignById);
-
+                    openAPIRouterBuilder.operation("campaignsGET").handler(apiHandler::campaignsGET);
+                    openAPIRouterBuilder.operation("campaignIdGET").handler(apiHandler::campaignIdGET);
                     openAPIRouterBuilder.operation("campaignIdUnitsGET").handler(apiHandler::campaignIdUnitsGET);
+                    openAPIRouterBuilder.operation("campaignIdUnitsObservationsGET").handler(apiHandler::campaignIdUnitsObservationsGET);
+                    openAPIRouterBuilder.operation("campaignIdUnitsObservationsLocationsGET").handler(apiHandler::campaignIdUnitsObservationsLocationsGET);
+
                     openAPIRouterBuilder.operation("campaignIdUnitIdObservationsGET").handler(apiHandler::campaignIdUnitIdObservationsGET);
-                    openAPIRouterBuilder.operation("campaignIdUnitsObservationsLocationGET").handler(apiHandler::campaignIdUnitsObservationsLocationGET);
                     openAPIRouterBuilder.operation("campaignIdUnitIdLocationsGET").handler(apiHandler::campaignIdUnitIdLocationsGET);
 
                     Router mainRouter = openAPIRouterBuilder.createRouter();

+ 161 - 70
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java

@@ -2,9 +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.JsonConvertor;
 import cz.senslog.telemetry.utils.TernaryCondition;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.json.JsonArray;
@@ -16,14 +14,12 @@ import org.apache.logging.log4j.Logger;
 
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-import static cz.senslog.telemetry.utils.JsonConvertor.floatArrToJson;
+import static java.lang.Boolean.parseBoolean;
 import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
 import static java.util.stream.Collectors.toList;
@@ -32,6 +28,11 @@ public class OpenAPIHandler {
     private static final Logger logger = LogManager.getLogger(OpenAPIHandler.class);
 
     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 BiFunction<OffsetDateTime, ZoneId, String> DATE_TIME_FORMATTER = (dateTime, zoneId) ->
+            OffsetDateTime.ofInstant(dateTime.toInstant(), zoneId).format(ISO_OFFSET_DATE_TIME);
 
     private final SensLogRepository repo;
 
@@ -57,56 +58,154 @@ public class OpenAPIHandler {
             ).encode());
     }
 
-    public void campaignAll(RoutingContext rc) {
+    public void campaignsGET(RoutingContext rc) {
         String host =  hostURLFull(rc.request());
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
         repo.allCampaigns()
                 .onSuccess(data -> rc.response().end(new JsonArray(
-                        data.stream().map(c -> JsonObject.of(
-                                        "Campaign@NavigationLink", String.format("%s/campaigns/%d", host, c.getId()),
-                                        "id", c.getId(),
-                                        "description", c.getDescription(),
-                                        "from_time", c.getFromTime().toString(),
-                                        "to_time", c.getToTime().toString()
-                                )).collect(toList())).encode()))
-                .onFailure(th -> { logger.catching(th); rc.fail(400, th); });
+                        data.stream().map(c -> (navigationLinks ? JsonObject.of(
+                                "Campaign@NavigationLink", String.format("%s/campaigns/%d", host, c.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", c.getId(),
+                                "description", c.getDescription(),
+                                "fromTime", DATE_TIME_FORMATTER.apply(c.getFromTime(), zone),
+                                "toTime", DATE_TIME_FORMATTER.apply(c.getToTime(), zone)
+                                ))).collect(toList())).encode()))
+                .onFailure(th -> rc.fail(400, th));
     }
 
-    public void campaignById(RoutingContext rc) {
+    public void campaignIdGET(RoutingContext rc) {
         String host =  hostURLFull(rc.request());
-        long campaignId = Long.parseLong(rc.pathParam("id"));
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
         repo.findCampaignById(campaignId)
-                .onSuccess(c -> repo.findUnitIdsByCampaignId(c.getId())
-                        .onSuccess(units -> rc.response().end(JsonObject.of(
-                                "Observations@NavigationLink", String.format("%s/campaigns/%d/observations",host, c.getId()),
+                .onSuccess(c -> rc.response().end((navigationLinks ? JsonObject.of(
+                                "self@NavigationLink", String.format("%s/campaigns/%d", host, c.getId()),
+                                "Observations@NavigationLink", String.format("%s/campaigns/%d/units/observations", host, c.getId()),
+                                "Locations@NavigationLink", String.format("%s/campaigns/%d/units/observations/locations?limitPerUnit=1", host, c.getId()),
+                                "Units@NavigationLink", String.format("%s/campaigns/%d/units", host, c.getId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
                                 "id", c.getId(),
                                 "description", c.getDescription(),
-                                "from_time", c.getFromTime().toString(),
-                                "to_time", c.getToTime().toString(),
-                                "units", units.stream().map(u -> JsonObject.of(
-                                        "Unit@NavigationLink", String.format("%s/units/%d", host, u),
-                                        "unit_id", u
-                                )).collect(toList())
-                            ).encode()))
-                )
+                                "fromTime", DATE_TIME_FORMATTER.apply(c.getFromTime(), zone),
+                                "toTime", DATE_TIME_FORMATTER.apply(c.getToTime(), zone)
+                        )).encode())
+                .onFailure(th -> rc.fail(400, th)));
+    }
+
+    public void campaignIdUnitsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = paramZone.isEmpty() ? DEFAULT_ZONE_ID : ZoneId.of(paramZone.get(0));
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
+
+        repo.findUnitsByCampaignId(campaignId)
+                .onSuccess(units -> rc.response().end(new JsonArray(
+                        units.stream().map(u -> (navigationLinks ? JsonObject.of(
+                                "Unit@NavigationLink", String.format("%s/units/%d",host, u.getUnitId())
+                        ) : JsonObject.of()).mergeIn(JsonObject.of(
+                                "id", u.getUnitId(),
+                                "description", u.getDescription(),
+                                "fromTime", DATE_TIME_FORMATTER.apply(u.getFromTime(), zone),
+                                "toTime", DATE_TIME_FORMATTER.apply(u.getToTime(), zone)
+                        ))).collect(toList())
+                ).encode()))
                 .onFailure(th -> rc.fail(400, th));
     }
 
-    public void campaignByIdObservations(RoutingContext rc) {
+    public void campaignIdUnitsObservationsGET(RoutingContext rc) {
         String host =  hostURLFull(rc.request());
-        long campaignId = Long.parseLong(rc.pathParam("id"));
-        repo.findObservationsByCampaignId(campaignId)
-                .onSuccess(observations -> rc.response().end(JsonObject.of(
-                        "Campaign@NavigationLink", String.format("%s/campaigns/%d",host, campaignId),
-                        "data", observations.stream().map(o -> JsonObject.of(
-                                "unit_id", o.getUnitId(),
-                                "time_stamp", o.getTimestamp().format(ISO_DATE_TIME),
-                                "location", JsonObject.of(
-                                        "longitude", o.getLongitude(),
-                                        "latitude", o.getLatitude(),
-                                        "altitude", o.getAltitude()),
-                                "observed_values", o.getObservedValues()
-                        )).collect(toList())
-                    ).encode()))
+        JsonObject paramsJson = new JsonObject();
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+
+        List<String> paramFrom = rc.queryParam("from");
+        OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> {
+            paramsJson.put("from", paramFrom.get(0));
+            return OffsetDateTime.parse(paramFrom.get(0));
+        });
+
+        List<String> paramTo = rc.queryParam("to");
+        OffsetDateTime to = TernaryCondition.<OffsetDateTime>ternaryIf(paramTo::isEmpty, () -> null,() -> {
+            paramsJson.put("to", paramTo.get(0));
+            return OffsetDateTime.parse(paramTo.get(0));
+        });
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> {
+            paramsJson.put("zone", paramZone.get(0));
+            return ZoneId.of(paramZone.get(0));
+        });
+
+        List<String> paramOffset = rc.queryParam("offset");
+        int offset = TernaryCondition.<Integer>ternaryIf(paramOffset::isEmpty, 0, () -> {
+            paramsJson.put("offset", paramOffset.get(0));
+            return Integer.parseInt(paramOffset.get(0));
+        });
+
+        List<String> paramLimit = rc.queryParam("limit");
+        int limit = TernaryCondition.<Integer>ternaryIf(paramLimit::isEmpty, DEFAULT_MAX_DATA_LIMIT, () -> {
+            paramsJson.put("limit", paramLimit.get(0));
+            return Integer.parseInt(paramLimit.get(0));
+        });
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
+            paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
+        });
+
+        JsonObject navLinks = navigationLinks ? JsonObject.of(
+                "Campaign@NavigationLink", String.format("%s/campaigns/%d",host, campaignId)
+        ) : JsonObject.of();
+
+        Function<Long, String> createNextNavLink = dataSize -> {
+            String urlParams = paramsJson.stream()
+                    .filter(e -> !e.getKey().equals("offset"))
+                    .map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
+                    .collect(Collectors.joining("&"));
+            long newOffset = offset + dataSize;
+            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)
+                .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(
+                                        "unitId", o.getUnitId(),
+                                        "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(th -> rc.fail(400, th));
     }
 
@@ -150,7 +249,7 @@ public class OpenAPIHandler {
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
-            return Boolean.parseBoolean(paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
         });
 
         JsonObject navLinks = navigationLinks ? JsonObject.of(
@@ -164,7 +263,7 @@ public class OpenAPIHandler {
                     .map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
                     .collect(Collectors.joining("&"));
             long newOffset = offset + dataSize;
-            return String.format("%s/campaigns/%d/units/%d/observations?offset=%d&%s", host, campaignId, unitId, newOffset, urlParams);
+            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)
@@ -180,36 +279,20 @@ public class OpenAPIHandler {
                                                 "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
                                                 "speed", o.getSpeed(),
                                                 "location", JsonObject.of(
-                                                        "longitude", o.getLongitude(),
-                                                        "latitude", o.getLatitude(),
-                                                        "altitude", o.getAltitude()),
+                                                        "longitude", o.getLocation().getLongitude(),
+                                                        "latitude", o.getLocation().getLatitude(),
+                                                        "altitude", o.getLocation().getAltitude()),
                                                 "observedValues", o.getObservedValues()
                                         )).collect(toList()))
                         )).encode()))
                 .onFailure(th -> rc.fail(400, th));
     }
 
-    public void campaignIdUnitsGET(RoutingContext rc) {
-        String host =  hostURLFull(rc.request());
-        long campaignId = Long.parseLong(rc.pathParam("id"));
-        repo.findUnitsByCampaignId(campaignId)
-                .onSuccess(units -> rc.response().end(new JsonArray(
-                        units.stream().map(u -> JsonObject.of(
-                                "Unit@NavigationLink", String.format("%s/units/%d",host, u.getUnitId()),
-                                "id", u.getUnitId(),
-                                "description", u.getDescription(),
-                                "fromTime", u.getFromTime().format(ISO_OFFSET_DATE_TIME),
-                                "toTime", u.getToTime().format(ISO_OFFSET_DATE_TIME)
-                        )).collect(toList())
-                ).encode()))
-                .onFailure(th -> rc.fail(400, th));
-    }
-
-    public void campaignIdUnitsObservationsLocationGET(RoutingContext rc) {
+    public void campaignIdUnitsObservationsLocationsGET(RoutingContext rc) {
         String host =  hostURLFull(rc.request());
         JsonObject paramsJson = new JsonObject();
 
-        long campaignId = Long.parseLong(rc.pathParam("id"));
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
         List<String> paramFrom = rc.queryParam("from");
         OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> {
             paramsJson.put("from", paramFrom.get(0));
@@ -243,7 +326,7 @@ public class OpenAPIHandler {
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
-            return Boolean.parseBoolean(paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
         });
 
         JsonObject navLinks = navigationLinks ? JsonObject.of(
@@ -257,7 +340,11 @@ public class OpenAPIHandler {
                         "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", floatArrToJson(l.getLocation())
+                            "location", JsonArray.of(
+                                        l.getLocation().getLongitude(),
+                                        l.getLocation().getLatitude(),
+                                        l.getLocation().getAltitude()
+                                )
                         )).collect(toList()))
                 )).encode()))
                 .onFailure(th -> rc.fail(400, th));
@@ -305,7 +392,7 @@ public class OpenAPIHandler {
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
-            return Boolean.parseBoolean(paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
         });
 
         JsonObject navLinks = navigationLinks ? JsonObject.of(
@@ -334,7 +421,11 @@ public class OpenAPIHandler {
                             "data", new JsonArray(
                                     paging.data().stream().map(l -> JsonObject.of(
                                             "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                            "location", floatArrToJson(l.getLocation())
+                                            "location", JsonArray.of(
+                                                    l.getLocation().getLongitude(),
+                                                    l.getLocation().getLatitude(),
+                                                    l.getLocation().getAltitude()
+                                            )
                                     )).collect(toList()))
                     )).encode()))
                     .onFailure(th -> rc.fail(400, th));

+ 7 - 7
src/main/java/cz/senslog/telemetry/utils/JsonConvertor.java → src/main/java/cz/senslog/telemetry/utils/ArrayToJsonConvertor.java

@@ -4,9 +4,9 @@ import io.vertx.core.json.JsonArray;
 
 import java.util.ArrayList;
 
-public final class JsonConvertor {
+public final class ArrayToJsonConvertor {
 
-    public static JsonArray byteArrToJson(byte[] arr) {
+    public static JsonArray byteArrToJson(byte... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (byte v : arr) {
             j.add(v);
@@ -14,7 +14,7 @@ public final class JsonConvertor {
         return j;
     }
 
-    public static JsonArray shortArrToJson(short[] arr) {
+    public static JsonArray shortArrToJson(short... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (short v : arr) {
             j.add(v);
@@ -22,7 +22,7 @@ public final class JsonConvertor {
         return j;
     }
 
-    public static JsonArray intArrToJson(int[] arr) {
+    public static JsonArray intArrToJson(int... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (int v : arr) {
             j.add(v);
@@ -30,7 +30,7 @@ public final class JsonConvertor {
         return j;
     }
 
-    public static JsonArray floatArrToJson(float[] arr) {
+    public static JsonArray floatArrToJson(float... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (float v : arr) {
             j.add(v);
@@ -38,7 +38,7 @@ public final class JsonConvertor {
         return j;
     }
 
-    public static JsonArray doubleArrToJson(double[] arr) {
+    public static JsonArray doubleArrToJson(double... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (double v : arr) {
             j.add(v);
@@ -46,7 +46,7 @@ public final class JsonConvertor {
         return j;
     }
 
-    public static JsonArray longArrToJson(long[] arr) {
+    public static JsonArray longArrToJson(long... arr) {
         JsonArray j = new JsonArray(new ArrayList(arr.length));
         for (long v : arr) {
             j.add(v);

+ 83 - 31
src/main/resources/openAPISpec.yaml

@@ -26,8 +26,26 @@ paths:
 
   /campaigns:
     get:
-      operationId: campaignAllGET
+      operationId: campaignsGET
       summary: Publish info about all campaigns
+      parameters:
+        - in: query
+          name: zone
+          schema:
+            type: string
+            default: UTC
+          required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
+        - in: query
+          name: navigationLinks
+          schema:
+            type: boolean
+            default: true
+          description: Option to disable @NavigationLinks in a response
       responses:
         200:
           description: JSON array of basic info of each campaign
@@ -44,18 +62,35 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
-  /campaigns/{id}:
+  /campaigns/{campaignId}:
     get:
       operationId: campaignIdGET
       summary: Publish info about a campaign
       parameters:
         - in: path
-          name: id
+          name: campaignId
           schema:
             type: integer
             format: int64
           required: true
           description: Numeric ID of the campaign to get
+        - in: query
+          name: zone
+          schema:
+            type: string
+            default: UTC
+          required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
+        - in: query
+          name: navigationLinks
+          schema:
+            type: boolean
+            default: true
+          description: Option to disable @NavigationLinks in a response
       responses:
         200:
           description: JSON object containing info about a campaign
@@ -70,18 +105,35 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
-  /campaigns/{id}/units:
+  /campaigns/{campaignId}/units:
     get:
       operationId: campaignIdUnitsGET
       summary: Publish info about the campaign's units
       parameters:
         - in: path
-          name: id
+          name: campaignId
           schema:
             type: integer
             format: int64
           required: true
           description: Numeric ID of the campaign to get
+        - in: query
+          name: zone
+          schema:
+            type: string
+            default: UTC
+          required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
+        - in: query
+          name: navigationLinks
+          schema:
+            type: boolean
+            default: true
+          description: Option to disable @NavigationLinks in a response
       responses:
         200:
           description: JSON Array containing info about campaign's units
@@ -98,13 +150,13 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
-  /campaigns/{id}/units/observations:
+  /campaigns/{campaignId}/units/observations:
     get:
       operationId: campaignIdUnitsObservationsGET
       summary: Publish info about all data of units merged together within the campaign
       parameters:
         - in: path
-          name: id
+          name: campaignId
           schema:
             type: integer
             format: int64
@@ -158,19 +210,29 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
-  /campaigns/{id}/units/observations/location:
+  /campaigns/{campaignId}/units/observations/locations:
     get:
-      operationId: campaignIdUnitsObservationsLocationGET
+      operationId: campaignIdUnitsObservationsLocationsGET
       summary: Publish info about all data of units merged together within the campaign
       parameters:
         - in: path
-          name: id
+          name: campaignId
           schema:
             type: integer
             format: int64
           required: true
           description: Numeric ID of the campaign to get
         - in: query
+          name: limitPerUnit
+          required: true
+          schema:
+            type: integer
+            default: 1
+          examples:
+            Last locations:
+              value: 1
+              summary: Last Locations of each unit
+        - in: query
           name: from
           schema:
             type: string
@@ -185,15 +247,11 @@ paths:
           required: false
           example: 2017-07-21T17:32:28Z
         - in: query
-          name: limitPerUnit
-          required: true
+          name: zone
           schema:
-            type: integer
-            default: 1
-          examples:
-            Last locations:
-              value: 1
-              summary: Last Locations of each unit
+            type: string
+            default: UTC
+          required: false
         - in: query
           name: sort
           schema:
@@ -206,12 +264,6 @@ paths:
             Ascending Order:
               value: asc
         - in: query
-          name: zone
-          schema:
-            type: string
-            default: UTC
-          required: false
-        - in: query
           name: navigationLinks
           schema:
             type: boolean
@@ -421,7 +473,6 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
-
   /campaigns/{campaignId}/units/{unitId}/sensors:
     get:
       operationId: campaignIdUnitIdSensorsGET
@@ -749,7 +800,6 @@ components:
     CampaignBasicInfo:
       type: object
       required:
-        - Campaign@NavigationLink
         - id
         - fromTime
         - toTime
@@ -778,9 +828,6 @@ components:
     CampaignDetailInfo:
       type: object
       required:
-        - self@NavigationLink
-        - Observations@NavigationLink
-        - Units@NavigationLink
         - id
         - fromTime
         - toTime
@@ -792,6 +839,10 @@ components:
           description: Navigation link to all campaign's data across all units
           type: string
           format: uri
+        Locations@NavigationLink:
+          description: Navigation link to all campaigns' locations across all units
+          type: string
+          format: uni
         Units@NavigationLink:
           description: Navigation link to campaign's assigned units
           type: string
@@ -813,7 +864,8 @@ components:
           format: date-time
       example:
         self@NavigationLink: "<domain>/campaigns/1"
-        Observations@NavigationLink: "<domain>/campaigns/1/observations"
+        Observations@NavigationLink: "<domain>/campaigns/1/units/observations"
+        Locations@NavigationLink: "<domain>/campaigns/1/units/observations/locations"
         Units@NavigationLink: "<domain>/campaigns/1/units"
         id: 1
         description: "Campaign example"
@@ -824,7 +876,7 @@ components:
     CampaignUnitBasicInfo:
       type: object
       required:
-        - Unit@NavigationLink
+        - id
         - fromTime
         - toTime
       properties:

+ 6 - 6
src/test/java/cz/senslog/telemetry/DataSet.java

@@ -1,6 +1,6 @@
 package cz.senslog.telemetry;
 
-import cz.senslog.telemetry.database.domain.ObsTelemetry;
+import cz.senslog.telemetry.database.domain.UnitTelemetry;
 import cz.senslog.telemetry.protocol.domain.AVLDriverActivityPacket;
 import cz.senslog.telemetry.protocol.domain.AVLPacket;
 import cz.senslog.telemetry.protocol.domain.IOProperty;
@@ -41,13 +41,13 @@ public class DataSet {
         }
     }
 
-    public static void evaluate(AVLTelemetryObservation expected, ObsTelemetry actual) {
+    public static void evaluate(AVLTelemetryObservation expected, UnitTelemetry actual) {
         assertEquals(expected.getTimestamp(), actual.getTimestamp().toInstant());
-        assertEquals(expected.getLongitude(), actual.getLongitude());
-        assertEquals(expected.getLatitude(), actual.getLatitude());
-        assertEquals(expected.getAltitude(), actual.getAltitude());
+        assertEquals(expected.getLongitude(), actual.getLocation().getLongitude());
+        assertEquals(expected.getLatitude(), actual.getLocation().getLatitude());
+        assertEquals(expected.getAltitude(), actual.getLocation().getAltitude());
+        assertEquals(expected.getAngle(), actual.getLocation().getAngle());
         assertEquals(expected.getSpeed(), actual.getSpeed());
-        assertEquals(expected.getAngle(), actual.getAngle());
         for (IOProperty ioExp : expected.getIoProperties()) {
             assertEquals(ioExp.getValue(), actual.getObservedValues().getValue(Integer.toString(ioExp.getId())),
                     String.format("Value does not match for the IO ID %d", ioExp.getId()));

+ 7 - 12
src/test/java/cz/senslog/telemetry/database/repository/TelemetryRepositoryTest.java

@@ -4,7 +4,8 @@ import cz.senslog.telemetry.BinaryDataSet;
 import cz.senslog.telemetry.DataSet;
 import cz.senslog.telemetry.TestPropertiesUtils;
 import cz.senslog.telemetry.app.PropertyConfig;
-import cz.senslog.telemetry.database.domain.ObsTelemetry;
+import cz.senslog.telemetry.database.domain.Location;
+import cz.senslog.telemetry.database.domain.UnitTelemetry;
 import cz.senslog.telemetry.database.domain.Unit;
 import io.vertx.core.Vertx;
 import io.vertx.core.json.JsonObject;
@@ -64,17 +65,11 @@ class TelemetryRepositoryTest {
 
     @Test
     void save(Vertx vertx, VertxTestContext testContext) {
-        ObsTelemetry obs = new ObsTelemetry();
-        obs.setUnitId(TEST_UNIT_ID);
-        obs.setObservedValues(JsonObject.of());
-        obs.setLongitude(49.7384f);
-        obs.setLatitude(13.3736f);
-        obs.setSpeed(0);
-        obs.setAltitude(0);
-        obs.setAngle(0);
-        obs.setTimestamp(OffsetDateTime.now());
-
-        List<ObsTelemetry> data = List.of(obs);
+
+        Location location = Location.of(49.7384f, 13.3736f, 0, 0);
+        UnitTelemetry obs = UnitTelemetry.of(TEST_UNIT_ID, OffsetDateTime.now(), location, 0, JsonObject.of());
+
+        List<UnitTelemetry> data = List.of(obs);
 
         MapLogRepository repo = MapLogRepository.create(pgPool);
         repo.saveAllTelemetry(data).onComplete(testContext.succeeding(res -> testContext.verify(() -> {

+ 3 - 3
src/test/java/cz/senslog/telemetry/server/Fm4exSocketHandlerTest.java

@@ -2,7 +2,7 @@ package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.BinaryDataSet;
 import cz.senslog.telemetry.DataSet;
-import cz.senslog.telemetry.database.domain.ObsTelemetry;
+import cz.senslog.telemetry.database.domain.UnitTelemetry;
 import cz.senslog.telemetry.database.domain.Sensor;
 import cz.senslog.telemetry.database.domain.Unit;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
@@ -30,7 +30,7 @@ class Fm4exSocketHandlerTest {
         BinaryDataSet<String> datasetIMEI = DataSet.IMEI_1();
         BinaryDataSet<AVLTelemetryObservation[]> datasetAVLData8Ex = DataSet.AVLData_codec8Ex_1();
 
-        ArgumentCaptor<List<ObsTelemetry>> obsTelemetryCapture = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<UnitTelemetry>> obsTelemetryCapture = ArgumentCaptor.forClass(List.class);
 
 
         MapLogRepository repo = mock(MapLogRepository.class);
@@ -60,7 +60,7 @@ class Fm4exSocketHandlerTest {
                 .onSuccess(ctx -> assertEquals(SUCCESS, ctx.getByte(0)))
                 .onFailure(Throwable::printStackTrace);
 
-        List<ObsTelemetry> persistValues = obsTelemetryCapture.getValue();
+        List<UnitTelemetry> persistValues = obsTelemetryCapture.getValue();
         assertEquals(datasetAVLData8Ex.getObject().length, persistValues.size());
 
         assertEquals(TEST_UNIT_ID, persistValues.get(0).getUnitId());