diff --git a/src/main/java/de/app/fivegla/controller/dto/request/ImageProcessingRequest.java b/src/main/java/de/app/fivegla/controller/dto/request/ImageProcessingRequest.java index 074356f..b8d095d 100644 --- a/src/main/java/de/app/fivegla/controller/dto/request/ImageProcessingRequest.java +++ b/src/main/java/de/app/fivegla/controller/dto/request/ImageProcessingRequest.java @@ -1,6 +1,6 @@ package de.app.fivegla.controller.dto.request; -import de.app.fivegla.controller.dto.request.inner.DroneImage; +import de.app.fivegla.controller.dto.request.inner.CameraImage; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Getter; @@ -21,11 +21,11 @@ public class ImageProcessingRequest { private String transactionId; @NotBlank - @Schema(description = "The id of the drone.") - private String droneId; + @Schema(description = "The id of the camera.") + private String cameraId; @Schema(description = "The images to process.") - private List images; + private List images; @Schema(description = "A custom group ID, which can be used to group devices / measurements. This is optional, if not set, the default group will be used.") protected String groupId; diff --git a/src/main/java/de/app/fivegla/controller/dto/request/StationaryImageProcessingRequest.java b/src/main/java/de/app/fivegla/controller/dto/request/StationaryImageProcessingRequest.java new file mode 100644 index 0000000..74d960e --- /dev/null +++ b/src/main/java/de/app/fivegla/controller/dto/request/StationaryImageProcessingRequest.java @@ -0,0 +1,28 @@ +package de.app.fivegla.controller.dto.request; + +import de.app.fivegla.controller.dto.request.inner.CameraImage; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * Request for image processing. + */ +@Getter +@Setter +@Schema(description = "Request for image processing.") +public class StationaryImageProcessingRequest { + + @NotBlank + @Schema(description = "The id of the camera.") + private String cameraId; + + @Schema(description = "The images to process.") + private List images; + + @Schema(description = "A custom group ID, which can be used to group devices / measurements. This is optional, if not set, the default group will be used.") + protected String groupId; +} diff --git a/src/main/java/de/app/fivegla/controller/dto/request/inner/DroneImage.java b/src/main/java/de/app/fivegla/controller/dto/request/inner/CameraImage.java similarity index 95% rename from src/main/java/de/app/fivegla/controller/dto/request/inner/DroneImage.java rename to src/main/java/de/app/fivegla/controller/dto/request/inner/CameraImage.java index eb50221..733ea12 100644 --- a/src/main/java/de/app/fivegla/controller/dto/request/inner/DroneImage.java +++ b/src/main/java/de/app/fivegla/controller/dto/request/inner/CameraImage.java @@ -9,7 +9,7 @@ @Getter @Setter @Schema(description = "A single image to process.") -public class DroneImage { +public class CameraImage { @Schema(description = "The channel of the image.") private ImageChannel imageChannel; diff --git a/src/main/java/de/app/fivegla/controller/tenant/ImagesController.java b/src/main/java/de/app/fivegla/controller/tenant/ImagesController.java index 368bce3..6a1f324 100644 --- a/src/main/java/de/app/fivegla/controller/tenant/ImagesController.java +++ b/src/main/java/de/app/fivegla/controller/tenant/ImagesController.java @@ -7,6 +7,7 @@ import de.app.fivegla.controller.api.BaseMappings; import de.app.fivegla.controller.dto.request.GetAllImagesForTransactionRequest; import de.app.fivegla.controller.dto.request.ImageProcessingRequest; +import de.app.fivegla.controller.dto.request.StationaryImageProcessingRequest; import de.app.fivegla.controller.dto.response.*; import de.app.fivegla.integration.imageprocessing.ImageProcessingIntegrationService; import de.app.fivegla.integration.imageprocessing.OrthophotoIntegrationService; @@ -81,7 +82,47 @@ public ResponseEntity processImage(@Valid @RequestBody @Para var group = groupService.getOrDefault(tenant, request.getGroupId()); var oids = new ArrayList(); request.getImages().forEach(droneImage -> { - var oid = imageProcessingIntegrationService.processImage(tenant, group, request.getTransactionId(), request.getDroneId(), droneImage.getImageChannel(), droneImage.getBase64Image()); + var oid = imageProcessingIntegrationService.processImage(tenant, group, request.getTransactionId(), request.getCameraId(), droneImage.getImageChannel(), droneImage.getBase64Image()); + oids.add(oid); + }); + return ResponseEntity.status(HttpStatus.CREATED).body(ImageProcessingResponse.builder() + .oids(oids) + .build()); + } + + /** + * Processes one or multiple images from the mica sense camera. + * + * @return HTTP status 200 if image was processed successfully. + */ + @Operation( + operationId = "images.process-stationary-image", + description = "Processes one or multiple stationary images from the mica sense camera.", + tags = BaseMappings.IMAGE_PROCESSING + ) + @ApiResponse( + responseCode = "201", + description = "Images were processed successfully.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ImageProcessingResponse.class) + ) + ) + @ApiResponse( + responseCode = "400", + description = "The request is invalid.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Response.class) + ) + ) + @PostMapping(value = "/stationary", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity processImage(@Valid @RequestBody @Parameter(description = "The image processing request.", required = true) StationaryImageProcessingRequest request, Principal principal) { + var tenant = validateTenant(tenantService, principal); + var group = groupService.getOrDefault(tenant, request.getGroupId()); + var oids = new ArrayList(); + request.getImages().forEach(droneImage -> { + var oid = imageProcessingIntegrationService.processStationaryImage(tenant, group, request.getCameraId(), droneImage.getImageChannel(), droneImage.getBase64Image()); oids.add(oid); }); return ResponseEntity.status(HttpStatus.CREATED).body(ImageProcessingResponse.builder() diff --git a/src/main/java/de/app/fivegla/integration/fiware/model/MicaSenseImage.java b/src/main/java/de/app/fivegla/integration/fiware/model/CameraImage.java similarity index 94% rename from src/main/java/de/app/fivegla/integration/fiware/model/MicaSenseImage.java rename to src/main/java/de/app/fivegla/integration/fiware/model/CameraImage.java index c713604..5ac365f 100644 --- a/src/main/java/de/app/fivegla/integration/fiware/model/MicaSenseImage.java +++ b/src/main/java/de/app/fivegla/integration/fiware/model/CameraImage.java @@ -10,12 +10,12 @@ * Represents a MicaSense image. */ @Slf4j -public record MicaSenseImage( +public record CameraImage( String id, String type, Attribute group, Attribute oid, - Attribute droneId, + Attribute cameraId, Attribute transactionId, Attribute imageChannel, Attribute base64encodedImage, @@ -33,7 +33,7 @@ public String asJson() { " \"type\":\"" + type.trim() + "\"," + " \"customGroup\":" + group.asJson().trim() + "," + " \"oid\":" + oid.asJson().trim() + "," + - " \"droneId\":" + droneId.asJson().trim() + "," + + " \"cameraId\":" + cameraId.asJson().trim() + "," + " \"transactionId\":" + transactionId.asJson().trim() + "," + " \"imageChannel\":" + imageChannel.asJson().trim() + "," + " \"base64encodedImage\":" + base64encodedImage.asJson().trim() + "," + diff --git a/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingFiwareIntegrationServiceWrapper.java b/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingFiwareIntegrationServiceWrapper.java index d681413..8213725 100644 --- a/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingFiwareIntegrationServiceWrapper.java +++ b/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingFiwareIntegrationServiceWrapper.java @@ -3,10 +3,11 @@ import de.app.fivegla.api.enums.EntityType; import de.app.fivegla.integration.fiware.FiwareEntityIntegrationService; -import de.app.fivegla.integration.fiware.model.MicaSenseImage; +import de.app.fivegla.integration.fiware.model.CameraImage; import de.app.fivegla.integration.fiware.model.internal.TextAttribute; import de.app.fivegla.persistence.entity.Group; import de.app.fivegla.persistence.entity.Image; +import de.app.fivegla.persistence.entity.StationaryImage; import de.app.fivegla.persistence.entity.Tenant; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,18 +27,18 @@ public class ImageProcessingFiwareIntegrationServiceWrapper { private String imagePathBaseUrl; /** - * Create a new drone device measurement in FIWARE. + * Create a new image device measurement in FIWARE. * * @param image the image to create the measurement for * @param transactionId the transaction id */ - public void createDroneDeviceMeasurement(Tenant tenant, Group group, String droneId, Image image, String transactionId) { - var deviceMeasurement = new MicaSenseImage( - tenant.getFiwarePrefix() + droneId, + public void createCameraImage(Tenant tenant, Group group, String cameraId, Image image, String transactionId) { + var deviceMeasurement = new CameraImage( + tenant.getFiwarePrefix() + cameraId, EntityType.MICASENSE_IMAGE.getKey(), new TextAttribute(group.getOid()), new TextAttribute(image.getOid()), - new TextAttribute(droneId), + new TextAttribute(cameraId), new TextAttribute(image.getTransactionId()), new TextAttribute(image.getChannel().name()), new TextAttribute(image.getBase64encodedImage()), @@ -48,4 +49,12 @@ public void createDroneDeviceMeasurement(Tenant tenant, Group group, String dron fiwareEntityIntegrationService.persist(tenant, group, deviceMeasurement); } + /** + * Create a new stationary image device measurement in FIWARE. + * + * @param micaSenseImage the image to create the measurement for + */ + public void createStationaryCameraImage(Tenant tenant, Group group, String cameraId, StationaryImage micaSenseImage) { + // FIXME: Save stationary image in FIWARE + } } diff --git a/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingIntegrationService.java b/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingIntegrationService.java index c865e17..0c48404 100644 --- a/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingIntegrationService.java +++ b/src/main/java/de/app/fivegla/integration/imageprocessing/ImageProcessingIntegrationService.java @@ -3,8 +3,10 @@ import de.app.fivegla.api.dto.SortableImageOids; import de.app.fivegla.integration.imageprocessing.dto.TransactionIdWithTheFirstImageTimestamp; import de.app.fivegla.persistence.ImageRepository; +import de.app.fivegla.persistence.StationaryImageRepository; import de.app.fivegla.persistence.entity.Group; import de.app.fivegla.persistence.entity.Image; +import de.app.fivegla.persistence.entity.StationaryImage; import de.app.fivegla.persistence.entity.Tenant; import de.app.fivegla.persistence.entity.enums.ImageChannel; import lombok.RequiredArgsConstructor; @@ -26,17 +28,18 @@ public class ImageProcessingIntegrationService { private final ImageProcessingFiwareIntegrationServiceWrapper fiwareIntegrationServiceWrapper; private final PersistentStorageIntegrationService persistentStorageIntegrationService; private final ImageRepository imageRepository; + private final StationaryImageRepository stationaryImageRepository; /** * Processes an image from the mica sense camera. * * @param transactionId The transaction id. * @param group The group. - * @param droneId The id of the drone. + * @param cameraId The id of the drone. * @param imageChannel The channel the image was taken with. * @param base64Image The base64 encoded tiff image. */ - public String processImage(Tenant tenant, Group group, String transactionId, String droneId, ImageChannel imageChannel, String base64Image) { + public String processImage(Tenant tenant, Group group, String transactionId, String cameraId, ImageChannel imageChannel, String base64Image) { var decodedImage = Base64.getDecoder().decode(base64Image); log.debug("Channel for the decodedImage: {}.", imageChannel); var point = exifDataIntegrationService.readLocation(decodedImage); @@ -44,7 +47,7 @@ public String processImage(Tenant tenant, Group group, String transactionId, Str image.setOid(UUID.randomUUID().toString()); image.setGroup(group); image.setTenant(tenant); - image.setDroneId(droneId); + image.setCameraId(cameraId); image.setTransactionId(transactionId); image.setChannel(imageChannel); image.setLongitude(point.getX()); @@ -53,7 +56,7 @@ public String processImage(Tenant tenant, Group group, String transactionId, Str image.setBase64encodedImage(base64Image); var micaSenseImage = imageRepository.save(image); log.debug("Image with oid {} added to the application data.", micaSenseImage.getOid()); - fiwareIntegrationServiceWrapper.createDroneDeviceMeasurement(tenant, group, droneId, micaSenseImage, transactionId); + fiwareIntegrationServiceWrapper.createCameraImage(tenant, group, cameraId, micaSenseImage, transactionId); persistentStorageIntegrationService.storeImage(transactionId, micaSenseImage); return micaSenseImage.getOid(); } @@ -113,4 +116,24 @@ public List listAllTransactionsForTenan return allTransactionsForTenant; } + public String processStationaryImage(Tenant tenant, Group group, String cameraId, ImageChannel imageChannel, String base64Image) { + var decodedImage = Base64.getDecoder().decode(base64Image); + log.debug("Channel for the decodedImage: {}.", imageChannel); + var point = exifDataIntegrationService.readLocation(decodedImage); + var image = new StationaryImage(); + image.setOid(UUID.randomUUID().toString()); + image.setGroup(group); + image.setTenant(tenant); + image.setCameraId(cameraId); + image.setChannel(imageChannel); + image.setLongitude(point.getX()); + image.setLatitude(point.getY()); + image.setMeasuredAt((Date.from(exifDataIntegrationService.readMeasuredAt(decodedImage)))); + image.setBase64encodedImage(base64Image); + var micaSenseImage = stationaryImageRepository.save(image); + log.debug("Image with oid {} added to the application data.", micaSenseImage.getOid()); + fiwareIntegrationServiceWrapper.createStationaryCameraImage(tenant, group, cameraId, micaSenseImage); + // FIXME Store image in persistent storage + return micaSenseImage.getOid(); + } } diff --git a/src/main/java/de/app/fivegla/persistence/StationaryImageRepository.java b/src/main/java/de/app/fivegla/persistence/StationaryImageRepository.java new file mode 100644 index 0000000..d1466a4 --- /dev/null +++ b/src/main/java/de/app/fivegla/persistence/StationaryImageRepository.java @@ -0,0 +1,12 @@ +package de.app.fivegla.persistence; + +import de.app.fivegla.persistence.entity.StationaryImage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository for the stationary image entity. + */ +@Repository +public interface StationaryImageRepository extends JpaRepository { +} diff --git a/src/main/java/de/app/fivegla/persistence/entity/Image.java b/src/main/java/de/app/fivegla/persistence/entity/Image.java index 927dd7d..88dc59b 100644 --- a/src/main/java/de/app/fivegla/persistence/entity/Image.java +++ b/src/main/java/de/app/fivegla/persistence/entity/Image.java @@ -24,10 +24,10 @@ public class Image extends BaseEntity { private String oid; /** - * The id of the drone. + * The id of the camera. */ - @Column(name = "drone_id", nullable = false) - private String droneId; + @Column(name = "camera_id", nullable = false) + private String cameraId; /** * The transaction id. @@ -97,6 +97,6 @@ public byte[] getImageAsRawData() { * @return the name of the image */ public String getFullFilename(Tenant tenant, String transactionId) { - return tenant.getTenantId() + "/" + transactionId + "/" + droneId + "_" + channel.name() + "_" + measuredAt.toInstant().getEpochSecond() + ".tiff"; + return tenant.getTenantId() + "/" + transactionId + "/" + cameraId + "_" + channel.name() + "_" + measuredAt.toInstant().getEpochSecond() + ".tiff"; } } diff --git a/src/main/java/de/app/fivegla/persistence/entity/StationaryImage.java b/src/main/java/de/app/fivegla/persistence/entity/StationaryImage.java new file mode 100644 index 0000000..66ad910 --- /dev/null +++ b/src/main/java/de/app/fivegla/persistence/entity/StationaryImage.java @@ -0,0 +1,86 @@ +package de.app.fivegla.persistence.entity; + +import de.app.fivegla.persistence.entity.enums.ImageChannel; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * Image. + */ +@Entity +@Getter +@Setter +@Table(name = "image") +public class StationaryImage extends BaseEntity { + + /** + * The oid of the image. + */ + @Column(name = "oid", nullable = false, unique = true) + private String oid; + + /** + * The id of the camera. + */ + @Column(name = "camera_id", nullable = false) + private String cameraId; + + /** + * The channel of the image since the value cannot be read from the EXIF. + */ + @Column(name = "image_channel", nullable = false) + @Enumerated(EnumType.STRING) + private ImageChannel channel; + + /** + * The base64 encoded tiff image. + */ + @Lob + @Column(name = "base64_encoded_image", nullable = false) + private String base64encodedImage; + + /** + * The time the image was taken. + */ + @Column(name = "measured_at", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date measuredAt; + + /** + * The location of the image. + */ + @Column(name = "longitude", nullable = false) + private double longitude; + + /** + * The location of the image. + */ + @Column(name = "latitude", nullable = false) + private double latitude; + + /** + * The group of the image. + */ + @ManyToOne + @JoinColumn(name = "group_id", nullable = false) + private Group group; + + /** + * The tenant of the image. + */ + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "tenant_id", nullable = false) + private Tenant tenant; + + /** + * Returns the name of the image. + * + * @return the name of the image + */ + public String getFullFilename(Tenant tenant) { + return tenant.getTenantId() + "/" + cameraId + "/" + channel.name() + "/" + measuredAt.toInstant().getEpochSecond() + ".tiff"; + } +} diff --git a/src/main/resources/db/changelog/V5__add_stationary_images.sql b/src/main/resources/db/changelog/V5__add_stationary_images.sql new file mode 100644 index 0000000..0208a49 --- /dev/null +++ b/src/main/resources/db/changelog/V5__add_stationary_images.sql @@ -0,0 +1,19 @@ +create table if not exists stationary_image +( + id int primary key auto_increment, + version datetime not null, + oid varchar(255) not null, + camera_id varchar(255) not null, + transaction_id varchar(255) not null, + image_channel varchar(40) not null, + base64_encoded_image text not null, + measured_at datetime not null, + longitude numeric(10, 6) not null, + latitude numeric(10, 6) not null, + group_id int not null, + tenant_id int not null, + foreign key (group_id) references group_for_tenant (id), + foreign key (tenant_id) references tenant (id) +); + +alter table image rename column drone_id to camera_id; \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yml b/src/main/resources/db/changelog/db.changelog-master.yml index 667acd9..0753dbd 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yml +++ b/src/main/resources/db/changelog/db.changelog-master.yml @@ -22,4 +22,10 @@ databaseChangeLog: author: Sascha Doemer | sascha.doemer@lmis.de changes: - sqlFile: - path: db/changelog/V4__add_registered_devices.sql \ No newline at end of file + path: db/changelog/V4__add_registered_devices.sql + - changeSet: + id: 5 + author: Sascha Doemer | sascha.doemer@lmis.de + changes: + - sqlFile: + path: db/changelog/V5__add_stationary_images.sql \ No newline at end of file