org.venice.beachfront.bfapi.services.SceneService.java Source code

Java tutorial

Introduction

Here is the source code for org.venice.beachfront.bfapi.services.SceneService.java

Source

/**
 * Copyright 2018, Radiant Solutions, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package org.venice.beachfront.bfapi.services;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

import org.geotools.geojson.feature.FeatureJSON;
import org.joda.time.DateTime;
import org.opengis.feature.simple.SimpleFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.venice.beachfront.bfapi.database.dao.SceneDao;
import org.venice.beachfront.bfapi.model.Scene;
import org.venice.beachfront.bfapi.model.exception.UserException;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vividsolutions.jts.geom.Geometry;

import model.logger.Severity;
import util.PiazzaLogger;

@Service
public class SceneService {
    public static final String PROVIDER_URL_PLANET = "planet";
    public static final String PROVIDER_URL_LOCALINDEX = "localindex";

    @Value("${ia.broker.activation-poll-interval-sec}")
    private int asyncActivationPollIntervalSeconds;
    @Value("${ia.broker.activation-poll-max-attempts}")
    private int asyncActivationPollMaxAttempts;
    @Value("${ia.broker.protocol}")
    private String iaBrokerProtocol;
    @Value("${ia.broker.server}")
    private String iaBrokerServer;
    @Value("${ia.broker.port}")
    private int iaBrokerPort;
    @Value("${DOMAIN}")
    private String bfDomain;
    @Value("${ia.broker.enabled-platforms}")
    private String enabledPlatformsConcatenated;

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private ExecutorService executor;
    @Autowired
    private PiazzaLogger piazzaLogger;
    @Autowired
    private SceneDao sceneDao;

    public void activateScene(Scene scene, String planetApiKey) throws UserException {
        if (!scene.getStatus().equals(Scene.STATUS_INACTIVE)) {
            piazzaLogger.log(String.format("Scene %s was not inactive. No need to reactivate.", scene.getSceneId()),
                    Severity.INFORMATIONAL);
            return;
        }

        String platform = Scene.parsePlatform(scene.getSceneId());
        String provider = this.getProviderUrlFragment(scene.getSceneId());
        String activationPath = String.format("%s/activate/%s/%s", provider, platform, scene.getExternalId());

        try {
            this.restTemplate.getForEntity(UriComponentsBuilder.newInstance().scheme(this.iaBrokerProtocol)
                    .host(this.iaBrokerServer).port(this.iaBrokerPort).path(activationPath)
                    .queryParam("PL_API_KEY", planetApiKey).build().toUri(), Object.class);
            piazzaLogger.log(String.format("Successfully requested Activation of Scene %s to URL %s",
                    scene.getSceneId(), activationPath), Severity.INFORMATIONAL);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(String.format("Error activating Scene %s with Code %s and Message %s",
                    scene.getSceneId(), exception.getRawStatusCode(), exception.getResponseBodyAsString()),
                    Severity.ERROR);

            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("Upstream error activating Planet scene. (%d) platform=%s id=%s",
                    exception.getStatusCode().value(), platform, scene.getExternalId());
            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }
    }

    /**
     * Gets the Scene object from the local scene database. This does not reach out to the IA-broker, but rather, the
     * Beachfront database.
     * <p>
     * The Scenes get written to this database as they are requested through Planet. If a Scene ID is requested for a
     * valid scene that has not been queried through planet, then that scene will not be present in the database.
     * <p>
     * The purpose of this function is to return scene geometries immediately through the database for fast and large
     * lookups of Job geometries, without having to broker each request back through the IA-Broker.
     * 
     * @param sceneId
     * @return
     */
    public Scene getSceneFromLocalDatabase(String sceneId) {
        return sceneDao.findBySceneId(sceneId);
    }

    /**
     * Gets Scene information from the IA-broker.
     * <p>
     * As part of this process, the Scene metadata will be written to the local Beachfront database.
     * 
     * @param sceneId
     *            The scene ID
     * @param planetApiKey
     *            The users planet key
     * @param withTides
     *            Include tides information or not
     * @return The Scene object
     */
    public Scene getScene(String sceneId, String planetApiKey, boolean withTides) throws UserException {
        piazzaLogger.log(String.format("Requesting Scene %s information.", sceneId), Severity.INFORMATIONAL);
        String provider = this.getProviderUrlFragment(sceneId);
        String platform = Scene.parsePlatform(sceneId);
        String externalId = Scene.parseExternalId(sceneId);

        String scenePath = String.format("%s/%s/%s", provider, platform, externalId);

        ResponseEntity<JsonNode> response;
        try {
            response = this.restTemplate.getForEntity(UriComponentsBuilder.newInstance()
                    .scheme(this.iaBrokerProtocol).host(this.iaBrokerServer).port(this.iaBrokerPort).path(scenePath)
                    .queryParam("PL_API_KEY", planetApiKey).queryParam("tides", withTides).build().toUri(),
                    JsonNode.class);
        } catch (HttpClientErrorException | HttpServerErrorException exception) {
            piazzaLogger.log(String.format("Error Requesting Information for Scene %s with Code %s and Message %s",
                    sceneId, exception.getRawStatusCode(), exception.getResponseBodyAsString()), Severity.ERROR);

            HttpStatus recommendedErrorStatus = exception.getStatusCode();
            if (recommendedErrorStatus.equals(HttpStatus.UNAUTHORIZED)) {
                recommendedErrorStatus = HttpStatus.BAD_REQUEST; // 401 Unauthorized logs out the client, and we don't
                // want that
            }

            String message = String.format("Upstream error getting Planet scene. (%d) platform=%s id=%s",
                    exception.getStatusCode().value(), platform, externalId);
            throw new UserException(message, exception.getMessage(), recommendedErrorStatus);
        }

        JsonNode responseJson = response.getBody();

        Scene scene = new Scene();
        try {
            piazzaLogger.log(String.format("Beginning parsing of successful response of Scene %s data.", sceneId),
                    Severity.INFORMATIONAL);
            scene.setRawJson(responseJson);
            scene.setSceneId(platform + ":" + responseJson.get("id").asText());
            scene.setCloudCover(responseJson.get("properties").get("cloudCover").asDouble());
            scene.setHorizontalAccuracy(responseJson.get("properties").get("srcHorizontalAccuracy").asText());
            scene.setResolution(responseJson.get("properties").get("resolution").asInt());
            scene.setCaptureTime(DateTime.parse(responseJson.get("properties").get("acquiredDate").asText()));
            scene.setSensorName(responseJson.get("properties").get("sensorName").asText());
            scene.setUri(UriComponentsBuilder.newInstance().scheme(this.iaBrokerProtocol).host(this.iaBrokerServer)
                    .port(this.iaBrokerPort).path(scenePath).toUriString());

            try {
                // The response from IA-Broker is a GeoJSON feature. Convert to Geometry.
                FeatureJSON featureReader = new FeatureJSON();
                String geoJsonString = new ObjectMapper().writeValueAsString(responseJson);
                SimpleFeature feature = featureReader.readFeature(geoJsonString);
                Geometry geometry = (Geometry) feature.getDefaultGeometry();
                scene.setGeometry(geometry);
            } catch (IOException exception) {
                String error = String.format(
                        "Could not convert Scene %s response to GeoJSON object. This scene will not store properly in the database.",
                        scene.getSceneId());
                piazzaLogger.log(error, Severity.ERROR);
                throw new UserException(error, exception, HttpStatus.INTERNAL_SERVER_ERROR);
            }

            String status = "active";
            if (platform.equals(Scene.PLATFORM_PLANET_RAPIDEYE)
                    || platform.equals(Scene.PLATFORM_PLANET_PLANETSCOPE)) {
                status = responseJson.get("properties").get("status").asText();
            } else {
                // Status active
            }

            scene.setStatus(status);

            if (withTides) {
                scene.setTide(responseJson.get("properties").get("currentTide").asDouble());
                scene.setTideMin24H(responseJson.get("properties").get("minimumTide24Hours").asDouble());
                scene.setTideMax24H(responseJson.get("properties").get("maximumTide24Hours").asDouble());
            }
        } catch (NullPointerException ex) {
            piazzaLogger.log(String.format(
                    "Error parsing of successful response from IA-Broker for Scene %s data with Error %s. Raw Response content: %s",
                    sceneId, ex.getMessage(), responseJson.toString()), Severity.ERROR);
            throw new UserException("Error parsing JSON Scene Response from Upstream Broker Service.", ex,
                    HttpStatus.INTERNAL_SERVER_ERROR);
        }

        piazzaLogger.log(String.format("Successfully parsed Scene metadata for Scene %s with Status %s", sceneId,
                scene.getStatus()), Severity.INFORMATIONAL);
        sceneDao.save(scene);
        return scene;
    }

    public CompletableFuture<Scene> asyncGetActiveScene(String sceneId, String planetApiKey, boolean withTides) {
        return CompletableFuture.supplyAsync(() -> {
            for (int i = 0; i < asyncActivationPollMaxAttempts; i++) {
                Scene updatedScene;

                try {
                    updatedScene = this.getScene(sceneId, planetApiKey, withTides);
                } catch (UserException e) {
                    throw new RuntimeException(e);
                }

                if (updatedScene.getStatus().equals(Scene.STATUS_ACTIVE)) {
                    return updatedScene;
                }

                try {
                    Thread.sleep(asyncActivationPollIntervalSeconds * 1000L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

            throw new RuntimeException(new UserException("Upstream server timed out", HttpStatus.GATEWAY_TIMEOUT));
        }, this.executor);
    }

    /**
     * Gets the download URI for the specified scene
     * 
     * @param scene
     * @param planetApiKey
     * @return
     */
    public URI getDownloadUri(Scene scene, String planetApiKey) {
        // https://bf-api.{domain}/scene/{id}/download?planet_api_key={key}
        return UriComponentsBuilder.newInstance().scheme("https").host("bf-api." + bfDomain)
                .pathSegment("scene", scene.getSceneId(), "download").queryParam("planet_api_key", planetApiKey)
                .build().toUri();
    }

    public List<String> getSceneInputFileNames(Scene scene) {
        switch (Scene.parsePlatform(scene.getSceneId())) {
        case Scene.PLATFORM_PLANET_RAPIDEYE:
        case Scene.PLATFORM_PLANET_PLANETSCOPE:
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_PLANET:
            return Arrays.asList("multispectral.TIF");
        case Scene.PLATFORM_LOCALINDEX_LANDSAT:
        case Scene.PLATFORM_PLANET_LANDSAT:
            return Arrays.asList("coastal.TIF", "swir1.TIF");
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_S3:
            return Arrays.asList("coastal.JP2", "swir1.JP2");
        }
        return new ArrayList<>();
    }

    public List<String> getSceneInputURLs(Scene scene) {
        switch (Scene.parsePlatform(scene.getSceneId())) {
        case Scene.PLATFORM_PLANET_RAPIDEYE:
        case Scene.PLATFORM_PLANET_PLANETSCOPE:
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_PLANET:
            return Arrays.asList(scene.getLocationProperty());
        case Scene.PLATFORM_LOCALINDEX_LANDSAT:
        case Scene.PLATFORM_PLANET_LANDSAT:
            return Arrays.asList(scene.getImageBand("coastal"), scene.getImageBand("swir1"));
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_S3:
            return Arrays.asList(scene.getImageBand("blue"), scene.getImageBand("nir"));
        }
        return new ArrayList<>();
    }

    public String getProviderUrlFragment(String sceneId) throws UserException {
        switch (Scene.parsePlatform(sceneId)) {
        case Scene.PLATFORM_PLANET_LANDSAT:
        case Scene.PLATFORM_PLANET_PLANETSCOPE:
        case Scene.PLATFORM_PLANET_RAPIDEYE:
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_PLANET:
        case Scene.PLATFORM_PLANET_SENTINEL_FROM_S3:
            return PROVIDER_URL_PLANET;
        case Scene.PLATFORM_LOCALINDEX_LANDSAT:
            return PROVIDER_URL_LOCALINDEX;
        }
        throw new UserException("Cannot get platform string for Scene: " + sceneId,
                HttpStatus.INTERNAL_SERVER_ERROR);
    }

    public List<String> getEnabledPlatforms() {
        Set<String> allowedPlatforms = new HashSet<String>(
                Arrays.asList(Scene.PLATFORM_PLANET_LANDSAT, Scene.PLATFORM_PLANET_PLANETSCOPE,
                        Scene.PLATFORM_PLANET_RAPIDEYE, Scene.PLATFORM_PLANET_SENTINEL_FROM_PLANET,
                        Scene.PLATFORM_PLANET_SENTINEL_FROM_S3, Scene.PLATFORM_LOCALINDEX_LANDSAT));
        String[] specifiedPlatforms = this.enabledPlatformsConcatenated.split(",");
        Set<String> calculatedPlatforms = new HashSet<String>();

        for (String platform : specifiedPlatforms) {
            if (allowedPlatforms.contains(platform)) {
                calculatedPlatforms.add(platform);
            } else {
                this.piazzaLogger.log("Unknown platform in enabled platforms config: " + platform,
                        Severity.WARNING);
            }
        }

        return new ArrayList<String>(calculatedPlatforms);
    }
}