Quellcode durchsuchen

Local dump of changes

Lukas Cerny vor 4 Monaten
Ursprung
Commit
32dbdb2973
22 geänderte Dateien mit 517 neuen und 257 gelöschten Zeilen
  1. 11 2
      init.sql
  2. 14 0
      src/main/java/cz/senslog/telemetry/database/DatabaseException.java
  3. 21 0
      src/main/java/cz/senslog/telemetry/database/SQLErrorState.java
  4. 5 0
      src/main/java/cz/senslog/telemetry/database/domain/Unit.java
  5. 25 12
      src/main/java/cz/senslog/telemetry/database/domain/UnitTelemetry.java
  6. 31 10
      src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java
  7. 2 2
      src/main/java/cz/senslog/telemetry/database/validation/UnitTelemetryValidation.java
  8. 22 13
      src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java
  9. 1 0
      src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java
  10. 1 0
      src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java
  11. 1 1
      src/main/java/cz/senslog/telemetry/server/ws/ContentType.java
  12. 112 44
      src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java
  13. 16 3
      src/main/java/cz/senslog/telemetry/server/ws/WSParameters.java
  14. 7 0
      src/main/java/cz/senslog/telemetry/utils/CascadeCondition.java
  15. 201 122
      src/main/resources/openAPISpec.yaml
  16. BIN
      src/main/resources/sample_trajectory.png
  17. 1 2
      src/test/java/cz/senslog/telemetry/DataSet.java
  18. 5 5
      src/test/java/cz/senslog/telemetry/MockSensLogRepository.java
  19. 13 13
      src/test/java/cz/senslog/telemetry/database/repository/MapLogRepositoryTest.java
  20. 18 18
      src/test/java/cz/senslog/telemetry/database/validation/UnitTelemetryValidationTest.java
  21. 1 1
      src/test/java/cz/senslog/telemetry/server/Fm4exSocketHandlerTest.java
  22. 9 9
      src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerTest.java

+ 11 - 2
init.sql

@@ -88,7 +88,7 @@ ALTER TABLE ONLY maplog.obs_telemetry ALTER COLUMN id SET DEFAULT nextval('maplo
 
 CREATE TABLE maplog.entity (
     id          INTEGER NOT NULL PRIMARY KEY,
-    identity    VARCHAR(30) NOT NULL,
+    identity    VARCHAR(50) NOT NULL,
     name        VARCHAR(100) NOT NULL
 );
 
@@ -253,7 +253,7 @@ CREATE TABLE maplog.unit_static_to_mobile (
     static_unit_id  BIGINT NOT NULL,
     mobile_unit_id  BIGINT NOT NULL,
     the_geom public.geometry NOT NULL,
-    PRIMARY KEY (static_unit_id, mobile_unit_id)
+    time_last_assignment TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
 );
 ALTER TABLE maplog.unit_static_to_mobile OWNER TO senslog;
 
@@ -342,6 +342,15 @@ CREATE TYPE order_status AS ENUM ('READY', 'MONITORING', 'DELIVERED');
 
 CREATE TYPE delivery_type AS ENUM ('SERVICE_FOFR', 'VEHICLE_CITROEN', 'VEHICLE_PEUGEOT');
 
+CREATE TABLE tracking.unit_to_delivery_type (
+    delivery_type delivery_type PRIMARY KEY NOT NULL,
+    unit_id BIGINT NOT NULL
+);
+ALTER TABLE tracking.unit_to_delivery_type OWNER TO senslog;
+
+ALTER TABLE ONLY tracking.unit_to_delivery_type ADD CONSTRAINT trkunitdelivery FOREIGN KEY (unit_id) REFERENCES maplog.unit(unit_id) ON UPDATE CASCADE ON DELETE CASCADE;
+
+
 CREATE TABLE tracking.record (
     id BIGINT NOT NULL PRIMARY KEY,
     unit_id BIGINT NOT NULL,

+ 14 - 0
src/main/java/cz/senslog/telemetry/database/DatabaseException.java

@@ -0,0 +1,14 @@
+package cz.senslog.telemetry.database;
+
+public class DatabaseException extends RuntimeException {
+
+    private final SQLErrorState errorState;
+    public DatabaseException(SQLErrorState errorState, String message) {
+        super(message);
+        this.errorState = errorState;
+    }
+
+    public SQLErrorState getErrorState() {
+        return errorState;
+    }
+}

+ 21 - 0
src/main/java/cz/senslog/telemetry/database/SQLErrorState.java

@@ -0,0 +1,21 @@
+package cz.senslog.telemetry.database;
+
+public enum SQLErrorState {
+    DUPLICATE_KEY   ("23505")
+
+    ;
+
+    private final String sqlState;
+    SQLErrorState(String sqlState) {
+        this.sqlState = sqlState;
+    }
+
+    public static SQLErrorState ofSQLState(String sqlState) {
+        for (SQLErrorState state : SQLErrorState.values()) {
+            if (state.sqlState.equals(sqlState)) {
+                return state;
+            }
+        }
+        return null;
+    }
+}

+ 5 - 0
src/main/java/cz/senslog/telemetry/database/domain/Unit.java

@@ -37,4 +37,9 @@ public class Unit {
     public String getDescription() {
         return description;
     }
+
+    @Override
+    public String toString() {
+        return Long.toString(unitId);
+    }
 }

+ 25 - 12
src/main/java/cz/senslog/telemetry/database/domain/UnitTelemetry.java

@@ -4,7 +4,6 @@ import io.vertx.core.json.JsonObject;
 
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Stream;
@@ -16,23 +15,33 @@ public class UnitTelemetry {
     private final OffsetDateTime timestamp;
     private final Location location;
     private final int speed;
-    private final JsonObject observedValues;
+    private final JsonObject observedValuesByName;
+    private final JsonObject observedValuesById;
 
-    public static UnitTelemetry of(long id, long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValues) {
-        return new UnitTelemetry(id, unitId, timestamp, location, speed, observedValues);
+    public static UnitTelemetry of(long id, long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValuesById, JsonObject observedValuesByName) {
+        return new UnitTelemetry(id, unitId, timestamp, location, speed, observedValuesById, observedValuesByName);
     }
 
-    public static UnitTelemetry of(long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValues) {
-        return of(-1, unitId, timestamp, location, speed, observedValues);
+    public static UnitTelemetry of(long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValuesById, JsonObject observedValuesByName) {
+        return new UnitTelemetry(-1, unitId, timestamp, location, speed, observedValuesById, observedValuesByName);
     }
 
-    private UnitTelemetry(long id, long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValues) {
+    public static UnitTelemetry ofByIDs(long id, long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValuesById) {
+        return new UnitTelemetry(id, unitId, timestamp, location, speed, observedValuesById, null);
+    }
+
+    public static UnitTelemetry ofByIDs(long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValuesById) {
+        return new UnitTelemetry(-1, unitId, timestamp, location, speed, observedValuesById, null);
+    }
+
+    private UnitTelemetry(long id, long unitId, OffsetDateTime timestamp, Location location, int speed, JsonObject observedValuesById, JsonObject observedValuesByName) {
         this.id = id;
         this.unitId = unitId;
         this.timestamp = timestamp;
         this.location = location;
         this.speed = speed;
-        this.observedValues = observedValues;
+        this.observedValuesById = observedValuesById;
+        this.observedValuesByName = observedValuesByName;
     }
 
     public long getId() {
@@ -55,8 +64,12 @@ public class UnitTelemetry {
         return speed;
     }
 
-    public JsonObject getObservedValues() {
-        return observedValues;
+    public JsonObject getObservedValuesByName() {
+        return observedValuesByName;
+    }
+
+    public JsonObject getObservedValuesById() {
+        return observedValuesById;
     }
 
     @Override
@@ -65,7 +78,7 @@ public class UnitTelemetry {
                 "\"id\":" + id +
                 ", \"timestamp\":" + "\"" + timestamp.format(DateTimeFormatter.ISO_DATE_TIME) + "\"" +
                 ", \"unitId\":" + unitId +
-                ", \"observedValues\":" + observedValues +
+                ", \"observedValues\":" + (observedValuesByName == null ? observedValuesById : observedValuesByName) +
                 ", \"location\":" + location +
                 ", \"speed\":" + speed +
                 '}';
@@ -90,7 +103,7 @@ public class UnitTelemetry {
             if (unitTelemetry == null) {
                 return Stream.empty();
             }
-            return unitTelemetry.getObservedValues().stream().map(sensorEntry -> SensLogObservation.of(
+            return unitTelemetry.getObservedValuesById().stream().map(sensorEntry -> SensLogObservation.of(
                     unitTelemetry.getUnitId(),
                     Integer.parseInt(sensorEntry.getKey()),
                     unitTelemetry.getTimestamp(),

+ 31 - 10
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -1,12 +1,16 @@
 package cz.senslog.telemetry.database.repository;
 
 import cz.senslog.telemetry.database.DataNotFoundException;
+import cz.senslog.telemetry.database.DatabaseException;
+import cz.senslog.telemetry.database.SQLErrorState;
 import cz.senslog.telemetry.database.SortType;
 import cz.senslog.telemetry.database.domain.*;
 import cz.senslog.telemetry.database.domain.Filter;
+import cz.senslog.telemetry.server.ws.HttpIllegalArgumentException;
 import io.vertx.core.Future;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
+import io.vertx.pgclient.PgException;
 import io.vertx.sqlclient.*;
 
 import java.time.OffsetDateTime;
@@ -36,6 +40,16 @@ public class MapLogRepository implements SensLogRepository {
         return this.client;
     }
 
+    private static <T> Future<T> failureHandler(Throwable exception) {
+        if (exception instanceof PgException pgException) {
+            SQLErrorState sqlErrorState = SQLErrorState.ofSQLState(pgException.getSqlState());
+            if (sqlErrorState != null) {
+                return Future.failedFuture(new DatabaseException(sqlErrorState, pgException.getDetail()));
+            }
+        }
+        return Future.failedFuture(exception);
+    }
+
     private static JsonObject convertSensorIdToName(Map<Long, String> sensors, JsonObject observedValues) {
         JsonObject res = new JsonObject(new HashMap<>(observedValues.size()));
         for (Map.Entry<String, Object> val : observedValues) {
@@ -118,7 +132,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false).map(r -> r.getLong(0)).collect(toList())));
     }
 
-    private final static Function<Row, UnitTelemetry> ROW_TO_UNIT_TELEMETRY = r -> UnitTelemetry.of(
+    private final static Function<Row, UnitTelemetry> ROW_TO_UNIT_TELEMETRY = r -> UnitTelemetry.ofByIDs(
             r.getLong("id"),
             r.getLong("unit_id"),
             r.getOffsetDateTime("time_stamp"),
@@ -149,7 +163,7 @@ public class MapLogRepository implements SensLogRepository {
                         data.getLocation().getAltitude(),
                         data.getLocation().getAngle(),
                         data.getSpeed(),
-                        data.getObservedValues()))
+                        data.getObservedValuesByName()))
                 .map(RowSet::iterator)
                 .map(it -> it.hasNext() ? ROW_TO_UNIT_TELEMETRY.apply(it.next()) : null);
     }
@@ -167,7 +181,7 @@ public class MapLogRepository implements SensLogRepository {
                 d.getLocation().getAltitude(),
                 d.getLocation().getAngle(),
                 d.getSpeed(),
-                d.getObservedValues()
+                d.getObservedValuesById()
         )).collect(toList());
         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::jsonb) " +
@@ -177,7 +191,7 @@ public class MapLogRepository implements SensLogRepository {
                                 "ST_Z (ST_Transform (the_geom, 4326)) AS alt, " +
                                 "ST_M (the_geom) AS angle")
                 .executeBatch(tuples)
-                .map(row -> StreamSupport.stream(row.spliterator(), false).map(r -> UnitTelemetry.of(
+                .map(row -> StreamSupport.stream(row.spliterator(), false).map(r -> UnitTelemetry.ofByIDs(
                         r.getLong("id"),
                         r.getLong("unit_id"),
                         r.getOffsetDateTime("time_stamp"),
@@ -189,7 +203,8 @@ public class MapLogRepository implements SensLogRepository {
                         ),
                         r.getInteger("speed"),
                         r.getJsonObject("observed_values")
-                )).toList());
+                )).toList())
+                .recover(MapLogRepository::failureHandler);
     }
 
     @Override
@@ -236,7 +251,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<StaticUnitBasicInfo> findMobileUnitByStaticUnitIdAndTimestamp(long unitId, OffsetDateTime timestamp) {
-        return client.preparedQuery("SELECT ustm.static_unit_id, ustm.mobile_unit_id, ustm.the_geom, utc.campaign_id, " +
+        return client.preparedQuery("SELECT ustm.static_unit_id, ustm.mobile_unit_id, utc.campaign_id, " +
                             "ST_X (ST_Transform (ustm.the_geom, 4326)) AS long, " +
                             "ST_Y (ST_Transform (ustm.the_geom, 4326)) AS lat, " +
                             "ST_Z (ST_Transform (ustm.the_geom, 4326)) AS alt, " +
@@ -244,9 +259,9 @@ public class MapLogRepository implements SensLogRepository {
                         "FROM maplog.unit_static_to_mobile ustm " +
                         "JOIN maplog.unit_to_campaign utc on utc.unit_id = mobile_unit_id " +
                         "JOIN maplog.campaign c on c.id = utc.campaign_id " +
-                        "WHERE ustm.static_unit_id = $1 " +
+                        "WHERE ustm.static_unit_id = $1 AND ustm.time_last_assignment <= $2 " +
                             "and c.from_time <= $2 and $2 <= (case when c.to_time is null then now() else c.to_time end) " +
-                        "LIMIT 1")
+                        "ORDER BY ustm.time_last_assignment DESC LIMIT 1")
                 .execute(Tuple.of(unitId, timestamp))
                 .map(RowSet::iterator)
                 .map(it -> it.hasNext() ? ROW_TO_STATIC_UNIT_BASIC_INFO.apply(it.next()) : null)
@@ -1471,7 +1486,7 @@ public class MapLogRepository implements SensLogRepository {
         return client.preparedQuery("SELECT s.sensor_id, s.name FROM maplog.sensor s " +
                 "JOIN maplog.unit_to_sensor uts ON uts.sensor_id = s.sensor_id " +
                 "JOIN maplog.unit_to_campaign utc ON utc.unit_id = uts.unit_id " +
-                "WHERE utc.campaign_id = $1")
+                "WHERE utc.campaign_id = $1 GROUP BY s.sensor_id;")
                 .execute(Tuple.of(campaignId))
                 .map(r -> StreamSupport.stream(r.spliterator(), false)
                         .collect(Collectors.toMap(row -> row.getLong("sensor_id"), row -> row.getString("name"))))
@@ -1488,6 +1503,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -1575,6 +1591,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -1659,6 +1676,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -1746,6 +1764,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -1826,6 +1845,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -1908,6 +1928,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getInteger("speed"),
+                                r.getJsonObject("observed_values"),
                                 convertSensorIdToName(sensors, r.getJsonObject("observed_values"))
                         ))
                         .collect(toList())
@@ -2588,7 +2609,7 @@ public class MapLogRepository implements SensLogRepository {
             row.getString("message"),
             row.getOffsetDateTime("time_stamp"),
             AlertStatus.valueOf(row.getString("status")),
-            row.getLong("telemetry_id") != null ? UnitTelemetry.of(
+            row.getLong("telemetry_id") != null ? UnitTelemetry.ofByIDs(
                     row.getLong("telemetry_id"),
                     row.getLong("unit_id"),
                     row.getOffsetDateTime("time_stamp"),

+ 2 - 2
src/main/java/cz/senslog/telemetry/database/validation/UnitTelemetryValidation.java

@@ -56,7 +56,7 @@ public class UnitTelemetryValidation {
             Map<String, String> sensorNames = unitSensors.item2();
 
             JsonObject newObservedValues = JsonObject.of();
-            for (Map.Entry<String, Object> observedValue : tel.getObservedValues()) {
+            for (Map.Entry<String, Object> observedValue : tel.getObservedValuesById()) {
                 String key = observedValue.getKey();
                 if (sensorIDs.contains(key)) {
                     newObservedValues.put(key, observedValue.getValue());
@@ -66,7 +66,7 @@ public class UnitTelemetryValidation {
             }
 
             if (!newObservedValues.isEmpty()) {
-                newTelemetries.add(UnitTelemetry.of(
+                newTelemetries.add(UnitTelemetry.ofByIDs(
                         tel.getUnitId(),
                         tel.getTimestamp(),
                         tel.getLocation(),

+ 22 - 13
src/main/java/cz/senslog/telemetry/server/Fm4exSocketHandler.java

@@ -17,7 +17,6 @@ import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
-import static cz.senslog.telemetry.module.EventBusModulePaths.SENSLOG_OBSERVATIONS;
 import static java.time.ZoneOffset.UTC;
 
 public class Fm4exSocketHandler {
@@ -40,7 +39,9 @@ public class Fm4exSocketHandler {
     }
 
     public Future<String> destroySocket(String socketID) {
-        sockets.forEach((k, v) -> logger.info("active SOCKET({})/UNIT({})", k, v.context().contextUnit));
+            sockets.forEach((k, v) -> logger.info("active UNIT({})/SOCKET({})",
+                    v.context().contextUnit != null ? v.context().contextUnit : "unknown", k));
+
         if (sockets.containsKey(socketID)) {
             return Future.succeededFuture(sockets.remove(socketID).context().socketId);
         }
@@ -55,8 +56,11 @@ public class Fm4exSocketHandler {
 
         private static final Logger logger = LogManager.getLogger(SocketContext.class);
 
-        private static final Buffer ERROR = Buffer.buffer().appendInt(0x00);
-        private static final Function<Integer, Buffer> MAKE_SUCCESS = count -> count > 0 ? Buffer.buffer().appendInt(count.byteValue()) : ERROR;
+        private static final Buffer ERROR_BYTE = Buffer.buffer().appendByte((byte)0x00);
+        private static final Buffer ERROR_INT = Buffer.buffer().appendInt(0x00);
+
+        private static final Buffer SUCCESS_BYTE = Buffer.buffer().appendByte((byte) 0x01);
+        private static final Function<Integer, Buffer> MAKE_SUCCESS_INT = count -> count > 0 ? Buffer.buffer().appendInt(count.byteValue()) : ERROR_INT;
 
         private final Vertx vertx;
 
@@ -79,7 +83,7 @@ public class Fm4exSocketHandler {
             try {
                 imei = Fm4ex.parseIMEI(buffer);
                 if (imei == null) {
-                    return Future.succeededFuture(ERROR);
+                    return Future.succeededFuture(ERROR_BYTE);
                 }
             } catch (RuntimeException e) {
                 return Future.failedFuture(e);
@@ -88,11 +92,11 @@ public class Fm4exSocketHandler {
                 if (u != null) {
                     contextUnit = u;
                     logger.info("[{}] Enabling IMEI '{}' as the unit '{}'.", socketId, u.getImei(), u.getUnitId());
-                    return MAKE_SUCCESS.apply(0x01);
+                    return SUCCESS_BYTE;
                 } else {
                     contextUnit = null;
                     logger.warn("[{}] Rejecting the IMEI '{}' due to: Unit does not exist.", socketId, imei);
-                    return ERROR;
+                    return ERROR_BYTE;
                 }
             });
         }
@@ -145,7 +149,7 @@ public class Fm4exSocketHandler {
 
             OffsetDateTime timestamp = avlPacket.getTimestamp().atZone(UTC).toOffsetDateTime();
             return repo.updateEvent(new EntityAction(contextUnit.getUnitId(), driverId, actionId, timestamp))
-                    .map(MAKE_SUCCESS)
+                    .map(MAKE_SUCCESS_INT)
                     .onSuccess(res -> logger.info("[{}] AVL Driver Activity Packet<{}> saved.", socketId, res));
         }
 
@@ -167,6 +171,7 @@ public class Fm4exSocketHandler {
                     for (IOProperty io : avl.getIoProperties()) {
                         Sensor sensor = ioToSensor.get(io.getId());
                         if (sensor != null) {
+                            logger.debug("[{}] sensor ID<{}> with value <{}>", socketId, sensor.getSensorId(), io.getValue());
                             observedValues.put(Long.toString(sensor.getSensorId()), io.getValue() * sensor.getIoMultiplier());
                          } else {
                             unknownIOs.add(io.getId());
@@ -175,19 +180,23 @@ public class Fm4exSocketHandler {
                     if (!unknownIOs.isEmpty()) {
                         logger.error("[{}] No sensors for IOs<{}>.", socketId, Arrays.toString(unknownIOs.toArray()));
                     }
-                    telemetries.add(UnitTelemetry.of(unitId, timestamp, location, avl.getSpeed(), observedValues));
+                    UnitTelemetry telemetry = UnitTelemetry.ofByIDs(unitId, timestamp, location, avl.getSpeed(), observedValues);
+                    logger.debug("[{}] Telemetry Observation [{}].", socketId, telemetry);
+                    telemetries.add(telemetry);
                 }
+                logger.info("Telemetries: {}", Arrays.toString(telemetries.toArray()));
                 return telemetries;
-            }).flatMap(data -> repo.saveAllTelemetry(data).map(count -> data))
+            }).flatMap(origObs -> repo.saveAllTelemetry(origObs).map(savedObs -> origObs))
                     .compose(data -> {
                         JsonArray sensLogObsArr = new JsonArray(UnitTelemetry.Converter
                                 .toSensLogObservationAsStream(data)
                                 .map(SensLogObservation::toJsonObject).toList()
                         );
-                        vertx.eventBus().request(SENSLOG_OBSERVATIONS, sensLogObsArr)
-                                .onSuccess(v -> logger.info(v.body())).onFailure(logger::error);
+                        // TODO enable if Analytics is used
+//                        vertx.eventBus().request(SENSLOG_OBSERVATIONS, sensLogObsArr)
+//                                .onSuccess(v -> logger.info(v.body())).onFailure(logger::error);
 
-                        return Future.succeededFuture(MAKE_SUCCESS.apply(data.size()));
+                        return Future.succeededFuture(MAKE_SUCCESS_INT.apply(data.size()));
                     }).onSuccess(resBuffer -> logger.info("[{}] AVL<{}> saved.", socketId, resBuffer.getByte(0)));
         }
     }

+ 1 - 0
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -146,6 +146,7 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.operation("integrationTrackingAllGET").handler(apiHandler::integrationTrackingAllGET);
                     openAPIRouterBuilder.operation("integrationTrackingStatusGET").handler(apiHandler::integrationTrackingStatusGET);
                     openAPIRouterBuilder.operation("integrationOrderCreateGET").handler(apiHandler::integrationOrderCreateGET);
+                    openAPIRouterBuilder.operation("integrationOrderStatusGET").handler(apiHandler::integrationOrderStatusGET);
                     openAPIRouterBuilder.operation("integrationTrackingChangeGET").handler(apiHandler::integrationTrackingChangeGET);
 
                     Router mainRouter = openAPIRouterBuilder.createRouter();

+ 1 - 0
src/main/java/cz/senslog/telemetry/server/TCPVertxServer.java

@@ -34,6 +34,7 @@ public final class TCPVertxServer extends AbstractVerticle {
                                     socket.write(res);
                                 })
                                 .onFailure(th -> {
+                                    socket.write(Buffer.buffer().appendByte((byte)0x00));
                                     logger.error("[{}] {}", socket.writeHandlerID(), th.getMessage());
                                     socHandler.destroySocket(socket.writeHandlerID())
                                             .onComplete(res -> socket.close());

+ 1 - 1
src/main/java/cz/senslog/telemetry/server/ws/ContentType.java

@@ -4,7 +4,7 @@ import java.util.Optional;
 
 public enum ContentType {
     JSON        ("application/json"),
-    GEOJSON     ("application/geojson"),
+    GEOJSON     ("application/geo+json"),
     JSON_SCHEMA ("application/json+schema"),
     HTML        ("text/html"),
     TEXT        ("text/plain"),

+ 112 - 44
src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java

@@ -2,13 +2,10 @@ package cz.senslog.telemetry.server.ws;
 
 import cz.senslog.telemetry.app.Application;
 import cz.senslog.telemetry.app.PropertyConfig;
-import cz.senslog.telemetry.database.DataNotFoundException;
-import cz.senslog.telemetry.database.PagingRetrieve;
-import cz.senslog.telemetry.database.SortType;
+import cz.senslog.telemetry.database.*;
 import cz.senslog.telemetry.database.domain.*;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
-import cz.senslog.telemetry.module.EventBusModulePaths;
 import cz.senslog.telemetry.utils.CascadeCondition;
 import cz.senslog.telemetry.utils.ResourcesUtils;
 import io.vertx.core.CompositeFuture;
@@ -23,6 +20,7 @@ import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.MIMEHeader;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.json.schema.common.JsonUtil;
+import io.vertx.pgclient.PgException;
 import io.vertx.sqlclient.RowSet;
 import io.vertx.sqlclient.Tuple;
 import org.apache.logging.log4j.LogManager;
@@ -31,13 +29,10 @@ import org.apache.pdfbox.Loader;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
-import org.apache.pdfbox.pdmodel.font.PDFont;
-import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
 import org.apache.pdfbox.pdmodel.font.PDType1Font;
 import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
 import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
 import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
-import org.apache.pdfbox.pdmodel.interactive.form.PDField;
 
 
 import java.io.*;
@@ -57,6 +52,8 @@ import static cz.senslog.telemetry.server.ws.AuthorizationScope.READ_PERSONAL;
 import static cz.senslog.telemetry.server.ws.AuthorizationType.BEARER;
 import static cz.senslog.telemetry.server.ws.AuthorizationType.NONE;
 import static cz.senslog.telemetry.server.ws.ContentType.*;
+import static cz.senslog.telemetry.server.ws.WSParameters.ObservationFormat.ID;
+import static cz.senslog.telemetry.server.ws.WSParameters.ObservationFormat.NAME;
 import static cz.senslog.telemetry.utils.ListUtils.*;
 import static io.vertx.core.http.HttpHeaders.*;
 import static java.lang.String.format;
@@ -67,7 +64,6 @@ import static java.util.Comparator.comparing;
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.*;
 import static org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode.APPEND;
-import static org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA;
 
 public class OpenAPIHandler {
 
@@ -210,6 +206,7 @@ public class OpenAPIHandler {
         int offset = params.queryParams().offset();
         int limit = params.queryParams().limit();
         ContentType format = params.queryParams().format();
+        WSParameters.ObservationFormat obsFormat = params.queryParams().observationFormat();
         List<Filter> filters = params.queryParams().filter();
         boolean navigationLinks = params.queryParams().navigationLinks();
 
@@ -231,7 +228,7 @@ public class OpenAPIHandler {
                 .ifThen(u -> u.hasPermissionScope(READ_PERSONAL), u -> repo.findObservationsByIdentityAndCampaignIdWithPaging(u.getId(), campaignId, from, to, offset, limit, filters))
                 .execute(r -> {
                     switch (format) {
-                        case JSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, JSON.contentType())
+                        case JSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, format.contentType())
                                         .end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                                                         "next@NavigationLink", createNextNavLink.apply(paging.size())) : JsonObject.of())
                                                 .mergeIn(JsonObject.of(
@@ -249,12 +246,15 @@ public class OpenAPIHandler {
                                                                                 "latitude", o.getLocation().getLatitude(),
                                                                                 "altitude", o.getLocation().getAltitude()
                                                                         ),
-                                                                        "observedValues", o.getObservedValues()
+                                                                        "observedValues", CascadeCondition.of(obsFormat)
+                                                                                .ifThen(f -> f.equals(NAME), f -> o.getObservedValuesByName())
+                                                                                .ifThen(f -> f.equals(ID), f -> o.getObservedValuesById())
+                                                                                .get()
                                                                 )).toList()
                                                         )
                                                 )).encode()))
                                 .onFailure(rc::fail);
-                        case GEOJSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, GEOJSON.contentType()).end(JsonObject.of(
+                        case GEOJSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, format.contentType()).end(JsonObject.of(
                                         "type", "FeatureCollection",
                                         "metadata", JsonObject.of(
                                                 "size", paging.size(),
@@ -276,7 +276,10 @@ public class OpenAPIHandler {
                                                                 "unitId", o.getUnitId(),
                                                                 "timestamp",  DATE_TIME_FORMATTER_AT_ZONE.apply(o.getTimestamp(), zone),
                                                                 "speed", o.getSpeed(),
-                                                                "observedValues", o.getObservedValues()
+                                                                "observedValues", CascadeCondition.of(obsFormat)
+                                                                        .ifThen(f -> f.equals(NAME), f -> o.getObservedValuesByName())
+                                                                        .ifThen(f -> f.equals(ID), f -> o.getObservedValuesById())
+                                                                        .get()
                                                         )
                                                 )).collect(toList()))).encode()))
                                 .onFailure(rc::fail);
@@ -301,6 +304,7 @@ public class OpenAPIHandler {
         int limit = params.queryParams().limit();
         SortType sort = params.queryParams().sort();
         ContentType format = params.queryParams().format();
+        WSParameters.ObservationFormat obsFormat = params.queryParams().observationFormat();
         List<Filter> filters = params.queryParams().filter();
         boolean navigationLinks = params.queryParams().navigationLinks();
 
@@ -338,7 +342,10 @@ public class OpenAPIHandler {
                                                                         "longitude", o.getLocation().getLongitude(),
                                                                         "latitude", o.getLocation().getLatitude(),
                                                                         "altitude", o.getLocation().getAltitude()),
-                                                                "observedValues", o.getObservedValues()
+                                                                "observedValues", CascadeCondition.of(obsFormat)
+                                                                        .ifThen(f -> f.equals(NAME), f -> o.getObservedValuesByName())
+                                                                        .ifThen(f -> f.equals(ID), f -> o.getObservedValuesById())
+                                                                        .get()
                                                         )).collect(toList())))).encode()))
                                 .onFailure(rc::fail);
                         case GEOJSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, format.contentType()).end(JsonObject.of(
@@ -363,7 +370,10 @@ public class OpenAPIHandler {
                                                                 "unitId", o.getUnitId(),
                                                                 "timestamp", DATE_TIME_FORMATTER_AT_ZONE.apply(o.getTimestamp(), zone),
                                                                 "speed", o.getSpeed(),
-                                                                "observedValues", o.getObservedValues()
+                                                                "observedValues", CascadeCondition.of(obsFormat)
+                                                                        .ifThen(f -> f.equals(NAME), f -> o.getObservedValuesByName())
+                                                                        .ifThen(f -> f.equals(ID), f -> o.getObservedValuesById())
+                                                                        .get()
                                                         )
                                                 )).collect(toList()))).encode()))
                                 .onFailure(rc::fail);
@@ -399,7 +409,7 @@ public class OpenAPIHandler {
                 .ifThen(u -> u.hasPermissionScope(READ_PERSONAL), u -> repo.findUnitsLocationsByIdentityAndCampaignId(u.getId(), campaignId, limitPerUnit, from, to, sortType, filters))
                 .execute(r -> {
                     switch (format) {
-                        case JSON ->  r.onSuccess(locations -> rc.response().putHeader(CONTENT_TYPE, JSON.contentType()).end(navLinks.mergeIn(JsonObject.of(
+                        case JSON ->  r.onSuccess(locations -> rc.response().putHeader(CONTENT_TYPE, format.contentType()).end(navLinks.mergeIn(JsonObject.of(
                                         "params", paramsJson,
                                         "size", locations.size(),
                                         "data", new JsonArray(locations.stream().map(l -> JsonObject.of(
@@ -412,7 +422,7 @@ public class OpenAPIHandler {
                                         )).toList()))).encode()))
                                 .onFailure(rc::fail);
                         case GEOJSON -> r.onSuccess(data -> Optional.of(data.stream().collect(groupingBy(UnitLocation::getUnitId))).ifPresent(unitLocation -> rc.response()
-                                        .putHeader(CONTENT_TYPE, GEOJSON.contentType()).end(JsonObject.of(
+                                        .putHeader(CONTENT_TYPE, format.contentType()).end(JsonObject.of(
                                                 "type", "FeatureCollection",
                                                 "metadata", JsonObject.of(
                                                         "limitPerUnit", limitPerUnit
@@ -1287,7 +1297,7 @@ public class OpenAPIHandler {
                                                                         "longitude", o.getLocation().getLongitude(),
                                                                         "latitude", o.getLocation().getLatitude(),
                                                                         "altitude", o.getLocation().getAltitude()),
-                                                                "observedValues", o.getObservedValues()
+                                                                "observedValues", o.getObservedValuesByName()
                                                         )).collect(toList())))).encode()))
                                 .onFailure(rc::fail);
                         case GEOJSON -> r.onSuccess(paging -> rc.response().putHeader(CONTENT_TYPE, format.contentType()).end(JsonObject.of(
@@ -1312,7 +1322,7 @@ public class OpenAPIHandler {
                                                                 "unitId", o.getUnitId(),
                                                                 "timestamp", DATE_TIME_FORMATTER_AT_ZONE.apply(o.getTimestamp(), zone),
                                                                 "speed", o.getSpeed(),
-                                                                "observedValues", o.getObservedValues()
+                                                                "observedValues", o.getObservedValuesByName()
                                                         )
                                                 )).collect(toList()))).encode()))
                                 .onFailure(rc::fail);
@@ -1420,6 +1430,16 @@ public class OpenAPIHandler {
                         .onFailure(rc::fail));
     }
 
+    private void failureHandler(Throwable th, Consumer<Throwable> contextFailure) {
+        Throwable newTh = th;
+        if (th instanceof DatabaseException dbTh) {
+            if (dbTh.getErrorState() == SQLErrorState.DUPLICATE_KEY) {
+                newTh = new HttpIllegalArgumentException(409, dbTh.getMessage());
+            }
+        }
+        contextFailure.accept(newTh);
+    }
+
     public void campaignIdUnitsObservationsPOST(RoutingContext rc) {
         AuthBearerUser user = AuthBearerUser.of(rc.user());
         WSParameters params = WSParameters.wrap(rc.queryParams(), rc.pathParams());
@@ -1432,6 +1452,7 @@ public class OpenAPIHandler {
             if (original.isEmpty()) { return Future.succeededFuture(emptyList()); }
             List<Future<UnitTelemetry>> futures = new ArrayList<>(original.size());
             Map<Long, List<UnitTelemetry>> staticUnitToTlm = new HashMap<>();
+            // group telemetries by unit_id that do not have location
             for (UnitTelemetry t : original) {
                 if (t.getLocation() == null) {
                     staticUnitToTlm.computeIfAbsent(t.getUnitId(), unitId -> new ArrayList<>()).add(t);
@@ -1440,6 +1461,7 @@ public class OpenAPIHandler {
                 }
             }
 
+            // find the nearest location to the timestamp by the unit or use default location
             for (Map.Entry<Long, List<UnitTelemetry>> entry : staticUnitToTlm.entrySet()) {
                 long unitId = entry.getKey();
                 for (UnitTelemetry telemetry : entry.getValue()) {
@@ -1447,7 +1469,8 @@ public class OpenAPIHandler {
                     Future<UnitTelemetry> future = repo.findMobileUnitByStaticUnitIdAndTimestamp(unitId, timestamp)
                             .compose(u -> repo.findNearestLocationToTimestampByCampaignIdAndUnitId(u.getCampaignId(), u.getMobileUnitId(), timestamp, timestamp.minusMinutes(minuteDiff), timestamp.plusMinutes(minuteDiff))
                                     .map(t -> t.map(UnitLocation::getLocation).orElseGet(u::getDefaultLocation))
-                                    .map(l -> UnitTelemetry.of(unitId, timestamp, l, 0, telemetry.getObservedValues())));
+                                    .map(l -> UnitTelemetry.ofByIDs(unitId, timestamp, l, 0, telemetry.getObservedValuesById())))
+                            .onFailure(th -> failureHandler(th, rc::fail));
                     futures.add(future);
                 }
             }
@@ -1467,8 +1490,9 @@ public class OpenAPIHandler {
                                                                     .toSensLogObservationAsStream(savedTelemetries)
                                                                     .map(SensLogObservation::toJsonObject).toList()
                                                             );
-                                                            vertx.eventBus().request(EventBusModulePaths.SENSLOG_OBSERVATIONS, sensLogObsArr)
-                                                                    .onSuccess(v -> logger.info(v.body())).onFailure(logger::error);
+                                                            // TODO enable if Analytics is used
+//                                                            vertx.eventBus().request(EventBusModulePaths.SENSLOG_OBSERVATIONS, sensLogObsArr)
+//                                                                    .onSuccess(v -> logger.info(v.body())).onFailure(logger::error);
                                                             rc.response().end(new JsonArray(savedTelemetries.stream().map(t -> JsonObject.of(
                                                                             "id", t.getId(),
                                                                             "unitId", t.getUnitId(),
@@ -1479,11 +1503,11 @@ public class OpenAPIHandler {
                                                                                     "latitude", t.getLocation().getLatitude(),
                                                                                     "altitude", t.getLocation().getAltitude()
                                                                             ),
-                                                                            "observedValues", t.getObservedValues()
+                                                                            "observedValues", t.getObservedValuesById()
                                                                     )).toList()
                                                             ).encode());
                                                         })
-                                                        .onFailure(rc::fail)
+                                                        .onFailure(th -> failureHandler(th, rc::fail))
                                                 )))));
 
         final BiFunction<String, Integer, OffsetDateTime> toTimestamp = (timestamp, epoch) -> {
@@ -1498,7 +1522,7 @@ public class OpenAPIHandler {
         switch (ContentType.ofType(rc.request().getHeader(CONTENT_TYPE))) {
             case JSON -> Optional.of(rc.body().asJsonArray())
                     .map(jsonArray -> jsonArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(tel -> UnitTelemetry.of(
+                            .map(tel -> UnitTelemetry.ofByIDs(
                                     tel.getLong("unitId"),
                                     toTimestamp.apply(tel.getString("timestamp"), tel.getInteger("epoch")),
                                     Optional.ofNullable(tel.getJsonObject("location")).map(location -> Location.of(
@@ -1513,7 +1537,7 @@ public class OpenAPIHandler {
 
             case GEOJSON -> Optional.of(rc.body().asJsonObject()).map(j -> j.getJsonArray("features"))
                     .map(jsonArray -> jsonArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(feature -> UnitTelemetry.of(
+                            .map(feature -> UnitTelemetry.ofByIDs(
                                     feature.getJsonObject("properties").getLong("unitId"),
                                     OffsetDateTime.parse(feature.getJsonObject("properties").getString("timestamp")),
                                     Optional.of(feature.getJsonObject("geometry").getJsonArray("coordinates")).map(location -> Location.of(
@@ -1558,7 +1582,7 @@ public class OpenAPIHandler {
                                                         "latitude", a.getObservation().getLocation().getLatitude(),
                                                         "altitude", a.getObservation().getLocation().getAltitude()
                                                 ),
-                                                "observedValues", a.getObservation().getObservedValues()
+                                                "observedValues", a.getObservation().getObservedValuesByName()
                                         )
                                 )).encode()))
                                 .onFailure(rc::fail)
@@ -1813,7 +1837,7 @@ public class OpenAPIHandler {
                 .onSuccess(id -> rc.response().end(JsonObject.of(
                         "id", id
                 ).encode()))
-                .onFailure(rc::fail);
+                .onFailure(th -> failureHandler(th, rc::fail));
     }
 
     public void alertIdPUT(RoutingContext rc) {
@@ -1825,7 +1849,7 @@ public class OpenAPIHandler {
                 .onSuccess(id -> rc.response().end(JsonObject.of(
                         "id", id
                 ).encode()))
-                .onFailure(rc::fail);
+                .onFailure(th -> failureHandler(th, rc::fail));
     }
 
     public void alertIdDELETE(RoutingContext rc) {
@@ -1837,7 +1861,7 @@ public class OpenAPIHandler {
                 .onSuccess(id -> rc.response().end(JsonObject.of(
                         "id", id
                 ).encode()))
-                .onFailure(rc::fail);
+                .onFailure(th -> failureHandler(th, rc::fail));
     }
 
     public void legacyInsertObservationsGET(RoutingContext rc) {
@@ -1863,7 +1887,7 @@ public class OpenAPIHandler {
                 .compose(u -> repo
                         .findNearestLocationToTimestampByCampaignIdAndUnitId(u.getCampaignId(), u.getMobileUnitId(), timestamp, timestamp.minusMinutes(minuteDiff), timestamp.plusMinutes(minuteDiff))
                         .map(t -> t.map(UnitLocation::getLocation).orElseGet(u::getDefaultLocation)))
-                .compose(l -> repo.saveTelemetry(UnitTelemetry.of(unitId, timestamp, l, 0, JsonObject.of(sensorIdStr, value))).map(r -> r.getId() > 0))
+                .compose(l -> repo.saveTelemetry(UnitTelemetry.ofByIDs(unitId, timestamp, l, 0, JsonObject.of(sensorIdStr, value))).map(r -> r.getId() > 0))
                 .onSuccess(res -> rc.response().end(res.toString()))
                 .onFailure(rc::fail);
     }
@@ -1885,15 +1909,15 @@ public class OpenAPIHandler {
                         .onSuccess(f -> rc.response().end(JsonObject.of(
                                 "total", f.list().stream().mapToLong(Long.class::cast).sum()
                         ).encode()))
-                        .onFailure(rc::fail)
+                        .onFailure(th -> failureHandler(th, rc::fail))
                 )
-                .onFailure(rc::fail);
+                .onFailure(th -> failureHandler(th, rc::fail));
 
         switch (ContentType.ofType(rc.request().getHeader(CONTENT_TYPE))) {
             case JSON -> ofNullable(rc.body().asJsonArray())
                     .map(arr -> arr.stream()
                             .filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(tel -> UnitTelemetry.of(
+                            .map(tel -> UnitTelemetry.ofByIDs(
                                     tel.getLong("unitId"),
                                     OffsetDateTime.parse(tel.getString("timestamp")),
                                     Optional.of(tel.getJsonObject("location")).map(location -> Location.of(
@@ -1907,7 +1931,7 @@ public class OpenAPIHandler {
 
             case GEOJSON -> Optional.of(rc.body().asJsonObject()).map(j -> j.getJsonArray("features", JsonArray.of()))
                     .map(jsonArray -> jsonArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(feature -> UnitTelemetry.of(
+                            .map(feature -> UnitTelemetry.ofByIDs(
                                     feature.getJsonObject("properties").getLong("unitId"),
                                     OffsetDateTime.parse(feature.getJsonObject("properties").getString("timestamp")),
                                     Optional.of(feature.getJsonObject("geometry").getJsonArray("coordinates")).map(location -> Location.of(
@@ -1943,7 +1967,7 @@ public class OpenAPIHandler {
                                 "latitude", org.getLocation().getLatitude(),
                                 "altitude", org.getLocation().getAltitude()
                         ),
-                        "observedValues", org.getObservedValues()
+                        "observedValues", org.getObservedValuesByName()
                 ));
             }
             rc.response().end(new JsonArray(resList).encode());
@@ -1971,7 +1995,7 @@ public class OpenAPIHandler {
         switch (ContentType.ofType(rc.request().getHeader(CONTENT_TYPE))) {
             case JSON -> Optional.of(rc.body().asJsonArray())
                     .map(jsonArray -> jsonArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(tel -> UnitTelemetry.of(
+                            .map(tel -> UnitTelemetry.ofByIDs(
                                     tel.getLong("unitId"),
                                     toTimestamp.apply(tel.getString("timestamp"), tel.getInteger("epoch")),
                                     Optional.of(tel.getJsonObject("location", defaultLocation)).map(location -> Location.of(
@@ -1986,7 +2010,7 @@ public class OpenAPIHandler {
 
             case GEOJSON -> Optional.of(rc.body().asJsonObject()).map(j -> j.getJsonArray("features"))
                     .map(jsonArray -> jsonArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast)
-                            .map(feature -> UnitTelemetry.of(
+                            .map(feature -> UnitTelemetry.ofByIDs(
                                     feature.getJsonObject("properties").getLong("unitId"),
                                     OffsetDateTime.parse(feature.getJsonObject("properties").getString("timestamp")),
                                     Optional.of(feature.getJsonObject("geometry").getJsonArray("coordinates")).map(location -> Location.of(
@@ -2111,7 +2135,7 @@ public class OpenAPIHandler {
                                                 .map(c -> Map.entry(c.sensorId(), row.getBoolean(Long.toString(c.sensorId()))))
                                                 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))
                                 )).toList()).map(certificateData::addViolationObservations)
-                ).compose(certificateData -> Future.succeededFuture(ResourcesUtils.getBytes("no_trajectory.png"))
+                ).compose(certificateData -> Future.succeededFuture(ResourcesUtils.getBytes("sample_trajectory.png"))
                         .map(certificateData::addTrajectoryImage)
                 ).onSuccess(certificateData -> {
                     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@@ -2284,7 +2308,7 @@ public class OpenAPIHandler {
         READY, MONITORING, DELIVERED
     }
 
-    private record TrackingRecord(long unitId, long orderId, TrackingStatus status, OffsetDateTime trackingStart) {
+    private record TrackingRecord(long unitId, long orderId, TrackingStatus status, String deliveryType, OffsetDateTime trackingStart) {
         boolean isTracking() {
             return status.equals(TrackingStatus.MONITORING);
         }
@@ -2417,7 +2441,7 @@ public class OpenAPIHandler {
                         "unitId", rec.unitId(),
                         "orderId", rec.orderId(),
                         "isDelivering", rec.isTracking(),
-                        "deliveryType", "SERVICE_FOFR"
+                        "deliveryType", rec.deliveryType()
                 ).encode());
 
         Consumer<TrackingRecord> replyConsumer = switch (acceptType) {
@@ -2431,7 +2455,7 @@ public class OpenAPIHandler {
         }
 
         MapLogRepository locRepo = (MapLogRepository) repo;
-        locRepo.rawPool().preparedQuery("SELECT t.unit_id, t.status, t.order_id, t.time_tracking_start FROM tracking.record t WHERE t.unit_id = $1 AND t.status != 'DELIVERED' LIMIT 1")
+        locRepo.rawPool().preparedQuery("SELECT t.unit_id, t.status, t.order_id, t.time_tracking_start, t.delivery_type FROM tracking.record t WHERE t.unit_id = $1 AND t.status != 'DELIVERED' LIMIT 1")
                 .execute(Tuple.of(unitId))
                 .map(RowSet::iterator)
                 .map(row -> row.hasNext() ? row.next() : null)
@@ -2440,12 +2464,33 @@ public class OpenAPIHandler {
                         r.getLong("unit_id"),
                         r.getLong("order_id"),
                         TrackingStatus.valueOf(r.getString("status")),
+                        r.getString("delivery_type"),
                         r.getOffsetDateTime("time_tracking_start"))
-                ).orElse(new TrackingRecord(unitId, -1L, TrackingStatus.DELIVERED, null)))
+                ).orElse(new TrackingRecord(unitId, -1L, TrackingStatus.DELIVERED, null,null)))
                 .onSuccess(replyConsumer::accept)
                 .onFailure(rc::fail);
     }
 
+    public void integrationOrderStatusGET(RoutingContext rc) {
+        long orderId = Long.parseLong(rc.queryParam("order_id").get(0));
+        MapLogRepository locRepo = (MapLogRepository) repo;
+        locRepo.rawPool().preparedQuery("SELECT order_id, unit_id, status, time_tracking_start, time_tracking_stop FROM tracking.record WHERE order_id = $1 LIMIT 1")
+                .execute(Tuple.of(orderId))
+                .map(RowSet::iterator)
+                .map(row -> row.hasNext() ? row.next() : null)
+                .map(Optional::ofNullable)
+                .map(p -> p.orElseThrow(() -> new HttpIllegalArgumentException(404, String.format("Order not found: %s", orderId))))
+                .onSuccess(res -> rc.response().putHeader(CONTENT_TYPE, JSON.contentType())
+                        .end(JsonObject.of(
+                                "orderId", res.getLong("order_id"),
+                                "unitId", res.getLong("unit_id"),
+                                "status", res.getString("status"),
+                                "trackingStart", DATE_TIME_FORMATTER.apply(res.getOffsetDateTime("time_tracking_start")),
+                                "trackingStop", DATE_TIME_FORMATTER.apply(res.getOffsetDateTime("time_tracking_stop"))
+                        ).encode()))
+                .onFailure(rc::fail);
+    }
+
     public void integrationOrderCreateGET(RoutingContext rc) {
         long unitId = Long.parseLong(rc.queryParam("unit_id").get(0));
         long orderId = Long.parseLong(rc.queryParam("order_id").get(0));
@@ -2454,8 +2499,31 @@ public class OpenAPIHandler {
 
 
         MapLogRepository locRepo = (MapLogRepository) repo;
-        locRepo.rawPool().withTransaction(tr -> tr.preparedQuery("INSERT INTO tracking.record(unit_id, order_id, tracking_id, delivery_type) VALUES ($1, $2, $3, $4) " +
-                        "RETURNING order_id, unit_id, tracking_id, time_received")
+        locRepo.rawPool().withTransaction(tr -> tr.preparedQuery("WITH change AS (\n" +
+                                "    INSERT INTO maplog.unit_static_to_mobile (time_last_assignment, static_unit_id, mobile_unit_id)\n" +
+                                "        SELECT now(), $1, unit_id\n" +
+                                "        FROM tracking.unit_to_delivery_type\n" +
+                                "        WHERE delivery_type = $4\n" +
+                                "          -- AND true/false if a change for static <-> mobile unit comparing to the last assignment\n" +
+                                "          AND (SELECT COALESCE((SELECT uttm.mobile_unit_id != utdt.unit_id\n" +
+                                "                                FROM maplog.unit_static_to_mobile uttm, tracking.unit_to_delivery_type utdt\n" +
+                                "                                WHERE static_unit_id = $1 AND delivery_type = $4\n" +
+                                "                                ORDER BY time_last_assignment DESC\n" +
+                                "                                LIMIT 1), (SELECT\n" +
+                                "                            (CASE WHEN count(*) = 0 THEN TRUE ELSE FALSE END)\n" +
+                                "                        FROM maplog.unit_static_to_mobile WHERE static_unit_id = 100 LIMIT 1\n" +
+                                "                        )) AS update\n" +
+                                "               )\n" +
+                                "    LIMIT 1 RETURNING mobile_unit_id AS unit_id\n" +
+                                "), mobile AS (\n" +
+                                "    select case\n" +
+                                "               when count(*) > 0 then (SELECT unit_id AS tracking_id FROM tracking.unit_to_delivery_type WHERE delivery_type = $4 LIMIT 1)\n" +
+                                "               when count(*) = 0 then $3\n" +
+                                "               end as tracking_id\n" +
+                                "    from tracking.unit_to_delivery_type WHERE delivery_type = $4 LIMIT 1\n" +
+                                ")\n" +
+                                "INSERT INTO tracking.record(unit_id, order_id, tracking_id, delivery_type) SELECT $1, $2, mobile.tracking_id, $4 FROM mobile\n" +
+                                "RETURNING order_id, unit_id, tracking_id, time_received;")
                 .execute(Tuple.of(unitId, orderId, trackingId, deliveryType))
                 .map(RowSet::iterator)
                 .map(row -> row.hasNext() ? row.next() : null)

+ 16 - 3
src/main/java/cz/senslog/telemetry/server/ws/WSParameters.java

@@ -6,9 +6,7 @@ import cz.senslog.telemetry.database.domain.Filter;
 import io.vertx.core.MultiMap;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
-import io.vertx.ext.web.RoutingContext;
 
-import javax.print.DocFlavor;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
@@ -17,19 +15,29 @@ import java.util.Map;
 import java.util.Set;
 
 import static cz.senslog.telemetry.server.ws.ContentType.JSON;
+import static cz.senslog.telemetry.server.ws.WSParameters.ObservationFormat.NAME;
 import static java.lang.Boolean.parseBoolean;
 import static java.util.Collections.emptyList;
 import static java.util.stream.Collectors.toSet;
 
 public class WSParameters {
 
+    public enum ObservationFormat {
+        NAME, ID
+
+        ;
+        public static ObservationFormat of(String value) {
+            return ObservationFormat.valueOf(value.toUpperCase());
+        }
+    }
+
     private static final int DEFAULT_MAX_DATA_LIMIT = 500;
     private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("UTC");
     private static final SortType DEFAULT_SORT_TYPE = SortType.ASC;
     private static final boolean DEFAULT_NAVIGATION_LINKS = true;
     private static final int DEFAULT_LIMIT_PER_UNIT = 1;
     private static final ContentType DEFAULT_RESPONSE_FORMAT = JSON;
-
+    private static final ObservationFormat DEFAULT_OBSERVATION_FORMAT = NAME;
 
     private final WSQueryParameters queryParams;
     private final WSPathParameters pathParams;
@@ -137,6 +145,11 @@ public class WSParameters {
             return backupAs("format", strParam != null ? ContentType.of(strParam) : DEFAULT_RESPONSE_FORMAT);
         }
 
+        public ObservationFormat observationFormat() {
+            String strParam = queryParams.get("observationFormat");
+            return backupAs("observationFormat", strParam != null ? ObservationFormat.of(strParam) : DEFAULT_OBSERVATION_FORMAT);
+        }
+
         public List<Filter> filter() {
             List<String> strParams = queryParams.getAll("filter");
             if (strParams.isEmpty()) {

+ 7 - 0
src/main/java/cz/senslog/telemetry/utils/CascadeCondition.java

@@ -12,6 +12,8 @@ public interface CascadeCondition {
         CascadeConditionBuilder<I, O> ifThen(Function<I, Boolean> condition, Function<I, O> consumer);
 
         void execute(Consumer<O> finalizer);
+
+        O get();
     }
 
     interface CascadeConditionNullable<I, O> {
@@ -80,5 +82,10 @@ public interface CascadeCondition {
                 throw new IllegalStateException("No define default action!");
             }
         }
+
+        @Override
+        public O get() {
+            return result;
+        }
     }
 }

+ 201 - 122
src/main/resources/openAPISpec.yaml

@@ -33,8 +33,8 @@ paths:
       tags:
         - Campaign
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -61,8 +61,8 @@ paths:
       tags:
         - Campaign
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -88,8 +88,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -117,8 +117,8 @@ paths:
       tags:
         - Observation
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/fromParam'
@@ -127,18 +127,19 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/observationFormatParam'
         - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
           description: JSON containing stream of telemetry data
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
             application/json:
               schema:
                 $ref: '#/components/schemas/CampaignObservationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
         default:
           description: unexpected error
           content:
@@ -150,21 +151,20 @@ paths:
       tags:
         - Observation
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
       requestBody:
-#        required: true
         content:
+#          application/geo+json:
+#            schema:
+#              $ref: '#/components/schemas/GeoFeatureCollectionUnit'
           application/json:
             schema:
               type: array
               description: JSON Array of Observations. The attribute 'timestamp' accepts only ISO8601 or the pattern 'yyyy-MM-dd[+][ ]HH:mm:ss'. The attribute can be substituted by 'epoch'.
               items:
                 $ref: '#/components/schemas/CampaignDataObservation'
-          application/geojson:
-            schema:
-              $ref: '#/components/schemas/GeoFeatureCollectionUnit'
       responses:
         200:
           description: JSON
@@ -174,6 +174,12 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/CampaignDataObservation'
+        409:
+          description: Conflict, duplicate observation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Duplicate'
         default:
           description: unexpected error
           content:
@@ -189,6 +195,9 @@ paths:
       requestBody:
         #        required: true
         content:
+          application/geo+json:
+            schema:
+              $ref: '#/components/schemas/GeoFeatureCollectionUnit'
           application/json:
             schema:
               type: array
@@ -196,9 +205,6 @@ paths:
               minLength: 1
               items:
                 $ref: '#/components/schemas/CampaignDataObservation'
-          application/geojson:
-            schema:
-              $ref: '#/components/schemas/GeoFeatureCollectionUnit'
       responses:
         200:
           description: JSON
@@ -222,8 +228,8 @@ paths:
       tags:
         - Locations
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/limitPerUnitParam'
@@ -238,7 +244,7 @@ paths:
         200:
           description: JSON containing stream of telemetry data
           content:
-            application/geojson:
+            application/geo+json:
               schema:
                 $ref: '#/components/schemas/GeoCampaignUnitsMultiLocations'
             application/json:
@@ -262,12 +268,13 @@ paths:
       requestBody:
         required: true
         content:
+          application/geo+json:
+            schema:
+              $ref: '#/components/schemas/GeoCampaignUnitsSingleLocations'
           application/json:
             schema:
               $ref: '#/components/schemas/CampaignUnitsLocationArray'
-          application/geojson:
-            schema:
-              $ref: '#/components/schemas/GeoCampaignUnitsSingleLocations'
+
       responses:
         200:
           description: JSON Object containing number of updated observations.
@@ -289,8 +296,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -317,8 +324,8 @@ paths:
       tags:
         - Observation
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -329,18 +336,20 @@ paths:
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/sortParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/observationFormatParam'
         - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
           description: JSON containing stream of telemetry data
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
             application/json:
               schema:
                 $ref: '#/components/schemas/CampaignUnitObservationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
+
         default:
           description: unexpected error
           content:
@@ -355,8 +364,8 @@ paths:
       tags:
         - Locations
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -372,12 +381,12 @@ paths:
         200:
           description: JSON containing stream of locations
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureUnitMultiLocation'
             application/json:
               schema:
                 $ref: '#/components/schemas/CampaignUnitLocationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureUnitMultiLocation'
         default:
           description: unexpected error
           content:
@@ -392,8 +401,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -421,8 +430,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -449,8 +458,8 @@ paths:
       tags:
         - Observation
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -467,12 +476,12 @@ paths:
         200:
           description: JSON containing stream of telemetry data
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
             application/json:
               schema:
                 $ref: '#/components/schemas/CampaignUnitSensorObservationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
         default:
           description: unexpected error
           content:
@@ -487,8 +496,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
@@ -514,8 +523,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -540,8 +549,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -568,8 +577,8 @@ paths:
       tags:
         - Campaign
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -597,8 +606,8 @@ paths:
       tags:
         - Entity
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -625,8 +634,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
@@ -652,8 +661,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/sensorIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -678,8 +687,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/sensorIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -707,8 +716,8 @@ paths:
       tags:
         - Phenomenon
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
@@ -734,8 +743,8 @@ paths:
       tags:
         - Phenomenon
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/phenomenonIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -760,8 +769,8 @@ paths:
       tags:
         - Sensor
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/phenomenonIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -788,8 +797,8 @@ paths:
       tags:
         - Entity
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
@@ -815,8 +824,8 @@ paths:
       tags:
         - Entity
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/navigationLinksParam'
@@ -841,8 +850,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/fromParam'
@@ -871,8 +880,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -898,8 +907,8 @@ paths:
       tags:
         - Action
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -927,8 +936,8 @@ paths:
       tags:
         - Action
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/fromParam'
@@ -957,8 +966,8 @@ paths:
       tags:
         - Action
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/actionIdParam'
@@ -984,8 +993,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/actionIdParam'
@@ -1013,8 +1022,8 @@ paths:
       tags:
         - Unit
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/actionIdParam'
@@ -1041,8 +1050,8 @@ paths:
       tags:
         - Action
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -1069,8 +1078,8 @@ paths:
       tags:
         - Event
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -1099,7 +1108,7 @@ paths:
       tags:
         - Event
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -1138,8 +1147,8 @@ paths:
       tags:
         - Event
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -1162,7 +1171,7 @@ paths:
       tags:
         - Event
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
       requestBody:
@@ -1171,7 +1180,7 @@ paths:
           application/json:
             schema:
               type: object
-              properties: 
+              properties:
                 toTime:
                   type: string
                   format: date-time
@@ -1179,9 +1188,9 @@ paths:
       responses:
         200:
           description: Returns IO of the adjusted event.
-          content: 
+          content:
             application/json:
-              schema: 
+              schema:
                 $ref: '#/components/schemas/ResponseSingleChange'
         default:
           description: unexpected error
@@ -1197,8 +1206,8 @@ paths:
       tags:
         - Observation
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -1213,12 +1222,12 @@ paths:
         200:
           description: JSON Object of telemetry observations
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
             application/json:
               schema:
                 $ref: '#/components/schemas/ActionEventObservationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureCollectionUnit'
         default:
           description: unexpected error
           content:
@@ -1233,8 +1242,8 @@ paths:
       tags:
         - Locations
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/fromParam'
@@ -1249,12 +1258,12 @@ paths:
         200:
           description: JSON containing stream of locations
           content:
+            application/geo+json:
+              schema:
+                $ref: '#/components/schemas/GeoFeatureUnitMultiLocation'
             application/json:
               schema:
                 $ref: '#/components/schemas/ActionEventLocationPaging'
-            application/geojson:
-              schema:
-                $ref: '#/components/schemas/GeoFeatureUnitMultiLocation'
         default:
           description: unexpected error
           content:
@@ -1268,8 +1277,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/alertIdParam'
         - $ref: '#/components/parameters/zoneParam'
@@ -1292,7 +1301,7 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/alertIdParam'
       requestBody:
@@ -1319,7 +1328,7 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/alertIdParam'
       responses:
@@ -1342,8 +1351,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/alertStatusParam'
@@ -1372,8 +1381,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/eventIdParam'
         - $ref: '#/components/parameters/alertIdParam'
@@ -1399,8 +1408,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/fromParam'
@@ -1429,7 +1438,7 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [write:infrastructure]
+        - bearerAuth: [ write:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
       requestBody:
@@ -1466,8 +1475,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
@@ -1499,8 +1508,8 @@ paths:
       tags:
         - Alert
       security:
-        - bearerAuth: [read:personal]
-        - bearerAuth: [read:infrastructure]
+        - bearerAuth: [ read:personal ]
+        - bearerAuth: [ read:infrastructure ]
       parameters:
         - $ref: '#/components/parameters/entityIdParam'
         - $ref: '#/components/parameters/actionIdParam'
@@ -1629,6 +1638,53 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
+  /integration/order/status:
+    get:
+      operationId: integrationOrderStatusGET
+      tags:
+        - Integration
+      parameters:
+        - in: query
+          name: order_id
+          required: true
+          schema:
+            type: integer
+            format: int64
+            minimum: 1
+      responses:
+        200:
+          description: JSON Object including current status of the order
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  orderId:
+                    type: integer
+                    format: int64
+                    minimum: 1
+                  unitId:
+                    type: integer
+                    format: int64
+                    minimum: 1
+                  status:
+                    enum:
+                      - READY
+                      - MONITORING
+                      - DELIVERED
+                  trackingStart:
+                    type: string
+                    format: date-time
+                  trackingEnd:
+                    type: string
+                    format: date-time
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
   /integration/tracking/all:
     get:
       operationId: integrationTrackingAllGET
@@ -1948,6 +2004,14 @@ components:
         enum: [json, geojson]
         default: json
       required: false
+    observationFormatParam:
+      in: query
+      name: observationFormat
+      schema:
+        type: string
+        enum: [ name, id ]
+        default: name
+      required: false
     alertStatusParam:
       in: query
       name: status
@@ -4158,3 +4222,18 @@ components:
       example:
         code: 404
         message: "Not Found"
+
+    Duplicate:
+      type: object
+      required:
+        - code
+        - message
+      properties:
+        code:
+          type: integer
+          format: int32
+        message:
+          type: string
+      example:
+        code: 409
+        message: "Key (unit_id, time_stamp)=(10000, 2023-12-07 21:51:21+00) already exists."

BIN
src/main/resources/sample_trajectory.png


+ 1 - 2
src/test/java/cz/senslog/telemetry/DataSet.java

@@ -1,7 +1,6 @@
 package cz.senslog.telemetry;
 
 import cz.senslog.telemetry.database.domain.UnitTelemetry;
-import cz.senslog.telemetry.protocol.Fm4exCodecType;
 import cz.senslog.telemetry.protocol.domain.AVLDriverActivityPacket;
 import cz.senslog.telemetry.protocol.domain.AVLPacket;
 import cz.senslog.telemetry.protocol.domain.IOProperty;
@@ -36,7 +35,7 @@ public class DataSet {
         assertEquals(expected.getAngle(), actual.getLocation().getAngle());
         assertEquals(expected.getSpeed(), actual.getSpeed());
         for (IOProperty ioExp : expected.getIoProperties()) {
-            assertEquals(ioExp.getValue(), actual.getObservedValues().getValue(Integer.toString(ioExp.getId())),
+            assertEquals(ioExp.getValue(), actual.getObservedValuesByName().getValue(Integer.toString(ioExp.getId())),
                     String.format("Value does not match for the IO ID %d", ioExp.getId()));
         }
     }

+ 5 - 5
src/test/java/cz/senslog/telemetry/MockSensLogRepository.java

@@ -39,12 +39,12 @@ public class MockSensLogRepository implements SensLogRepository {
 
     @Override
     public Future<UnitTelemetry> saveTelemetry(UnitTelemetry data) {
-        return Future.succeededFuture(UnitTelemetry.of(1L, 1L, OffsetDateTime.now(), Location.of(10f, 10f, 10f, 10f), 0, JsonObject.of()));
+        return Future.succeededFuture(UnitTelemetry.ofByIDs(1L, 1L, OffsetDateTime.now(), Location.of(10f, 10f, 10f, 10f), 0, JsonObject.of()));
     }
 
     @Override
     public Future<List<UnitTelemetry>> saveAllTelemetry(List<UnitTelemetry> data) {
-        return Future.succeededFuture(List.of(UnitTelemetry.of(1L, 1L, OffsetDateTime.now(), Location.of(10f, 10f, 10f, 10f), 0, JsonObject.of())));
+        return Future.succeededFuture(List.of(UnitTelemetry.ofByIDs(1L, 1L, OffsetDateTime.now(), Location.of(10f, 10f, 10f, 10f), 0, JsonObject.of())));
     }
 
     @Override
@@ -475,9 +475,9 @@ public class MockSensLogRepository implements SensLogRepository {
         Location location = Location.of(14f, 49f, 450f, 0);
         JsonObject observedValues = JsonObject.of("105", 0);
         return List.of(
-                UnitTelemetry.of(1, 1000L, baseTimestamp.plusMinutes(1), location, 10, observedValues),
-                UnitTelemetry.of(2, 1000L, baseTimestamp.plusMinutes(2), location, 10, observedValues),
-                UnitTelemetry.of(3, 1000L, baseTimestamp.plusMinutes(3), location, 10, observedValues)
+                UnitTelemetry.ofByIDs(1, 1000L, baseTimestamp.plusMinutes(1), location, 10, observedValues),
+                UnitTelemetry.ofByIDs(2, 1000L, baseTimestamp.plusMinutes(2), location, 10, observedValues),
+                UnitTelemetry.ofByIDs(3, 1000L, baseTimestamp.plusMinutes(3), location, 10, observedValues)
         );
     }
 

+ 13 - 13
src/test/java/cz/senslog/telemetry/database/repository/MapLogRepositoryTest.java

@@ -259,7 +259,7 @@ class MapLogRepositoryTest {
             assertThat(t1.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t1.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t1.getSpeed()).isEqualTo(0);
-            assertThat(t1.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t1.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(1001)", 11.11,
                     "sensor(1002)", 12.12
             ));
@@ -277,7 +277,7 @@ class MapLogRepositoryTest {
             assertThat(t5.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t5.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t5.getSpeed()).isEqualTo(0);
-            assertThat(t5.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t5.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(1002)", 0d
             ));
 
@@ -535,7 +535,7 @@ class MapLogRepositoryTest {
             assertThat(t1.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t1.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t1.getSpeed()).isEqualTo(0);
-            assertThat(t1.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t1.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(2001)", 21.21,
                     "sensor(2002)", 22.22
             ));
@@ -549,7 +549,7 @@ class MapLogRepositoryTest {
             assertThat(t5.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t5.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t5.getSpeed()).isEqualTo(0);
-            assertThat(t5.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t5.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(2002)", 0d
             ));
 
@@ -810,7 +810,7 @@ class MapLogRepositoryTest {
             assertThat(t1.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t1.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t1.getSpeed()).isEqualTo(0);
-            assertThat(t1.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t1.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(1001)", 11.11,
                     "sensor(1002)",12.12
             ));
@@ -824,7 +824,7 @@ class MapLogRepositoryTest {
             assertThat(t5.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t5.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t5.getSpeed()).isEqualTo(0);
-            assertThat(t5.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t5.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(1002)", 0d
             ));
 
@@ -1057,7 +1057,7 @@ class MapLogRepositoryTest {
             assertThat(t1.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t1.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t1.getSpeed()).isEqualTo(0);
-            assertThat(t1.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t1.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(3001)", JsonArray.of(1, 2, 3, 4)
             ));
 
@@ -1102,7 +1102,7 @@ class MapLogRepositoryTest {
             assertThat(t1.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t1.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t1.getSpeed()).isEqualTo(0);
-            assertThat(t1.getObservedValues()).isEqualTo(JsonObject.of(
+            assertThat(t1.getObservedValuesByName()).isEqualTo(JsonObject.of(
                     "sensor(2001)", 21.21,
                     "sensor(2002)", 22.22
             ));
@@ -1116,7 +1116,7 @@ class MapLogRepositoryTest {
             assertThat(t5.getLocation().getAltitude()).isEqualTo(488f);
             assertThat(t5.getLocation().getAngle()).isEqualTo(0f);
             assertThat(t5.getSpeed()).isEqualTo(0);
-            assertThat(t5.getObservedValues()).isEqualTo(JsonObject.of());
+            assertThat(t5.getObservedValuesByName()).isEqualTo(JsonObject.of());
 
             testContext.completeNow();
         })));
@@ -3826,7 +3826,7 @@ class MapLogRepositoryTest {
     @DisplayName("DB Test: saveTelemetry#1")
     void saveTelemetry_NM1(Vertx vertx, VertxTestContext testContext) {
 
-        UnitTelemetry telemetry = UnitTelemetry.of(
+        UnitTelemetry telemetry = UnitTelemetry.ofByIDs(
                 1000L,
                 OffsetDateTime.of(2024, 5, 1, 0, 0, 0, 0, UTC),
                 Location.of(13.9f, 49.33f, 488f, 0f),
@@ -3840,7 +3840,7 @@ class MapLogRepositoryTest {
             assertThat(data.getTimestamp()).isEqualTo(telemetry.getTimestamp());
             assertThat(data.getLocation()).isEqualTo(telemetry.getLocation());
             assertThat(data.getSpeed()).isEqualTo(telemetry.getSpeed());
-            assertThat(data.getObservedValues()).isEqualTo(telemetry.getObservedValues());
+            assertThat(data.getObservedValuesByName()).isEqualTo(telemetry.getObservedValuesByName());
 
             testContext.completeNow();
         })));
@@ -3850,7 +3850,7 @@ class MapLogRepositoryTest {
     @DisplayName("DB Test: saveAllTelemetry#1")
     void saveAllTelemetry_NM1(Vertx vertx, VertxTestContext testContext) {
 
-        UnitTelemetry telemetry = UnitTelemetry.of(
+        UnitTelemetry telemetry = UnitTelemetry.ofByIDs(
                 1000L,
                 OffsetDateTime.of(2024, 5, 1, 1, 0, 0, 0, UTC),
                 Location.of(13.9f, 49.33f, 488f, 0f),
@@ -3869,7 +3869,7 @@ class MapLogRepositoryTest {
             assertThat(t.getTimestamp()).isEqualTo(telemetry.getTimestamp());
             assertThat(t.getLocation()).isEqualTo(telemetry.getLocation());
             assertThat(t.getSpeed()).isEqualTo(telemetry.getSpeed());
-            assertThat(t.getObservedValues()).isEqualTo(telemetry.getObservedValues());
+            assertThat(t.getObservedValuesByName()).isEqualTo(telemetry.getObservedValuesByName());
 
             testContext.completeNow();
         })));

+ 18 - 18
src/test/java/cz/senslog/telemetry/database/validation/UnitTelemetryValidationTest.java

@@ -22,10 +22,10 @@ class UnitTelemetryValidationTest {
         JsonObject observedValues = JsonObject.of();
 
         Campaign campaign = Campaign.of(1, "mock(name)", "mock(description)", baseTime, baseTime.plusMonths(1));
-        UnitTelemetry t1 = UnitTelemetry.of(1000L, baseTime, location, 0, observedValues);
-        UnitTelemetry t2 = UnitTelemetry.of(2000L, baseTime.plusMonths(1).minusSeconds(1), location, 0, observedValues);
-        UnitTelemetry t3 = UnitTelemetry.of(3000L, baseTime.plusMonths(1), location, 0, observedValues);
-        UnitTelemetry t4 = UnitTelemetry.of(4000L, baseTime.plusMonths(1).plusSeconds(1), location, 0, observedValues);
+        UnitTelemetry t1 = UnitTelemetry.ofByIDs(1000L, baseTime, location, 0, observedValues);
+        UnitTelemetry t2 = UnitTelemetry.ofByIDs(2000L, baseTime.plusMonths(1).minusSeconds(1), location, 0, observedValues);
+        UnitTelemetry t3 = UnitTelemetry.ofByIDs(3000L, baseTime.plusMonths(1), location, 0, observedValues);
+        UnitTelemetry t4 = UnitTelemetry.ofByIDs(4000L, baseTime.plusMonths(1).plusSeconds(1), location, 0, observedValues);
 
         assertTrue(UnitTelemetryValidation.isTelemetryWithinCampaign(campaign, t1));
         assertTrue(UnitTelemetryValidation.isTelemetryWithinCampaign(campaign, t2));
@@ -42,12 +42,12 @@ class UnitTelemetryValidationTest {
         Campaign campaign = Campaign.of(1, "mock(name)", "mock(description)", baseTime, baseTime.plusMonths(1));
 
         List<UnitTelemetry> telemetries = List.of(
-                UnitTelemetry.of(1000L, baseTime, location, 0, observedValues),
-                UnitTelemetry.of(2000L, baseTime.plusMinutes(1), location, 0, observedValues),
-                UnitTelemetry.of(3000L, baseTime.plusMinutes(1), location, 0, observedValues),
-                UnitTelemetry.of(4000L, baseTime.plusMonths(1).minusSeconds(1), location, 0, observedValues),
-                UnitTelemetry.of(5000L, baseTime.plusMonths(1), location, 0, observedValues),
-                UnitTelemetry.of(6000L, baseTime.plusMonths(1).plusSeconds(1), location, 0, observedValues)
+                UnitTelemetry.ofByIDs(1000L, baseTime, location, 0, observedValues),
+                UnitTelemetry.ofByIDs(2000L, baseTime.plusMinutes(1), location, 0, observedValues),
+                UnitTelemetry.ofByIDs(3000L, baseTime.plusMinutes(1), location, 0, observedValues),
+                UnitTelemetry.ofByIDs(4000L, baseTime.plusMonths(1).minusSeconds(1), location, 0, observedValues),
+                UnitTelemetry.ofByIDs(5000L, baseTime.plusMonths(1), location, 0, observedValues),
+                UnitTelemetry.ofByIDs(6000L, baseTime.plusMonths(1).plusSeconds(1), location, 0, observedValues)
         );
 
         List<UnitTelemetry> filteredTels = UnitTelemetryValidation.telemetriesWithinCampaign(campaign, telemetries);
@@ -75,15 +75,15 @@ class UnitTelemetryValidationTest {
         );
 
         List<UnitTelemetry> telemetries = List.of(
-                UnitTelemetry.of(2000L, baseTimestamp, location, 0, JsonObject.of(
+                UnitTelemetry.ofByIDs(2000L, baseTimestamp, location, 0, JsonObject.of(
                         "100", 0,
                         "200", 0
                 )),
-                UnitTelemetry.of(1000L, baseTimestamp.plusHours(1), location, 0, JsonObject.of(
+                UnitTelemetry.ofByIDs(1000L, baseTimestamp.plusHours(1), location, 0, JsonObject.of(
                         "Sensor Name 100", 0,
                         "Sensor Name 200", 0
                 )),
-                UnitTelemetry.of(3000, baseTimestamp, location, 0, JsonObject.of())
+                UnitTelemetry.ofByIDs(3000, baseTimestamp, location, 0, JsonObject.of())
         );
 
         List<UnitTelemetry> filteredTels = UnitTelemetryValidation.telemetriesAccToSensors(sensorsByUnitId, telemetries);
@@ -92,13 +92,13 @@ class UnitTelemetryValidationTest {
 
         UnitTelemetry t1 = filteredTels.get(0);
         assertEquals(2000L, t1.getUnitId());
-        assertEquals(1, t1.getObservedValues().size());
-        assertTrue(t1.getObservedValues().containsKey("100"));
+        assertEquals(1, t1.getObservedValuesByName().size());
+        assertTrue(t1.getObservedValuesByName().containsKey("100"));
 
         UnitTelemetry t2 = filteredTels.get(1);
         assertEquals(1000L, t2.getUnitId());
-        assertEquals(2, t2.getObservedValues().size());
-        assertTrue(t2.getObservedValues().containsKey("100"));
-        assertTrue(t2.getObservedValues().containsKey("200"));
+        assertEquals(2, t2.getObservedValuesByName().size());
+        assertTrue(t2.getObservedValuesByName().containsKey("100"));
+        assertTrue(t2.getObservedValuesByName().containsKey("200"));
     }
 }

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

@@ -54,7 +54,7 @@ class Fm4exSocketHandlerTest {
                 )));
 
         Mockito.when(repo.saveAllTelemetry(obsTelemetryCapture.capture()))
-                .thenReturn(Future.succeededFuture(Stream.of(datasetAVLData8Ex.getObject()).map(o -> UnitTelemetry.of(
+                .thenReturn(Future.succeededFuture(Stream.of(datasetAVLData8Ex.getObject()).map(o -> UnitTelemetry.ofByIDs(
                         1L,
                         1L,
                         OffsetDateTime.ofInstant(o.getTimestamp(), UTC),

+ 9 - 9
src/test/java/cz/senslog/telemetry/server/ws/OpenAPIHandlerTest.java

@@ -2393,7 +2393,7 @@ class OpenAPIHandlerTest {
                 Arguments.of(
                         Future.succeededFuture(
                                 CampaignUnitAlert.of(1, 1, 10, "mock(message)", baseTimestamp, AlertStatus.CREATED,
-                                        UnitTelemetry.of(1, 10, baseTimestamp, location, 0, JsonObject.of())
+                                        UnitTelemetry.ofByIDs(1, 10, baseTimestamp, location, 0, JsonObject.of())
                                 )
                         ), SUCCESS, JSON
                 ),
@@ -2432,8 +2432,8 @@ class OpenAPIHandlerTest {
         return Stream.of(
                 Arguments.of(
                         Future.succeededFuture(new PagingRetrieve<>(false, 2, List.of(
-                                UnitTelemetry.of(1, 10, baseTimestamp, location, 0, JsonObject.of()),
-                                UnitTelemetry.of(1, 10, baseTimestamp.plusMinutes(1), location, 0, JsonObject.of())
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp, location, 0, JsonObject.of()),
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp.plusMinutes(1), location, 0, JsonObject.of())
                         ))), SUCCESS, JSON
                 ),
                 Arguments.of(
@@ -2558,8 +2558,8 @@ class OpenAPIHandlerTest {
         return Stream.of(
                 Arguments.of(
                         Future.succeededFuture(new PagingRetrieve<>(false, 2, List.of(
-                                UnitTelemetry.of(1, 10, baseTimestamp, location, 0, JsonObject.of()),
-                                UnitTelemetry.of(1, 10, baseTimestamp.plusHours(1), location, 0, JsonObject.of())
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp, location, 0, JsonObject.of()),
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp.plusHours(1), location, 0, JsonObject.of())
                         ))), SUCCESS, JSON
                 ),
                 Arguments.of(
@@ -2600,8 +2600,8 @@ class OpenAPIHandlerTest {
         return Stream.of(
                 Arguments.of(
                         Future.succeededFuture(new PagingRetrieve<>(false, 2, List.of(
-                                UnitTelemetry.of(1, 10, baseTimestamp, location, 0, JsonObject.of()),
-                                UnitTelemetry.of(1, 10, baseTimestamp.plusHours(1), location, 0, JsonObject.of())
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp, location, 0, JsonObject.of()),
+                                UnitTelemetry.ofByIDs(1, 10, baseTimestamp.plusHours(1), location, 0, JsonObject.of())
                         ))), SUCCESS, JSON
                 ),
                 Arguments.of(
@@ -2939,7 +2939,7 @@ class OpenAPIHandlerTest {
         ArgumentCaptor<UnitTelemetry> obsTelemetryCapture = ArgumentCaptor.forClass(UnitTelemetry.class);
 
         when(repo.saveTelemetry(obsTelemetryCapture.capture()))
-                .thenReturn(Future.succeededFuture(UnitTelemetry.of(
+                .thenReturn(Future.succeededFuture(UnitTelemetry.ofByIDs(
                         1L,
                         staticSensorId,
                         unitTimestamp,
@@ -2965,7 +2965,7 @@ class OpenAPIHandlerTest {
                     assertThat(savedTelemetry.getTimestamp()).isEqualTo(unitTimestamp);
                     assertThat(savedTelemetry.getLocation()).isEqualTo(lastMobileUnitLocation);
                     assertThat(savedTelemetry.getSpeed()).isEqualTo(0);
-                    assertThat(savedTelemetry.getObservedValues()).isEqualTo(JsonObject.of(Long.toString(sensorId), sensorValue));
+                    assertThat(savedTelemetry.getObservedValuesByName()).isEqualTo(JsonObject.of(Long.toString(sensorId), sensorValue));
 
                     testContext.completeNow();
                 })));