    001// License: GPL. For details, see LICENSE file.
    002package org.openstreetmap.josm.plugins.streetside.utils.api;
    003
    004import java.lang.reflect.Array;
    005import java.util.function.Function;
    006
    007import javax.json.JsonArray;
    008import javax.json.JsonNumber;
    009import javax.json.JsonObject;
    010import javax.json.JsonString;
    011import javax.json.JsonValue;
    012
    013import org.openstreetmap.josm.data.coor.LatLon;
    014import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
    015import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
    016import org.openstreetmap.josm.plugins.streetside.utils.StreetsideURL.APIv3;
    017
    018/**
    019 * Decodes the JSON returned by {@link APIv3} into Java objects.
    020 * Takes a {@link JsonObject} and {@link #decodeSequence(JsonObject)} tries to convert it to a {@link StreetsideSequence}.
    021 */
    022public final class JsonSequencesDecoder {
    023  private JsonSequencesDecoder() {
    024    // Private constructor to avoid instantiation
    025  }
    026
    027  /**
    028   * Parses a given {@link JsonObject} as a GeoJSON Feature into a {@link StreetsideSequence}.
    029   * @param json the {@link JsonObject} to be parsed
    030   * @return a {@link StreetsideSequence} that is parsed from the given {@link JsonObject}. If mandatory information is
    031   *         missing from the JSON or it's not meeting the expecting format in another way, <code>null</code> will be
    032   *         returned.
    033   */
    034  public static StreetsideSequence decodeSequence(final JsonObject json) {
    035    if (json == null || !"Feature".equals(json.getString("type", null))) {
    036      return null;
    037    }
    038    StreetsideSequence result = null;
    039    final JsonObject properties = json.getJsonObject("properties");
    040    final Long ca = properties == null ? null : JsonDecoder.decodeTimestamp(properties.getString("cd", null));
    041    if (properties != null && properties.getString("id", null) != null && properties.getString("user_key", null) != null && ca != null) {
    042      result = new StreetsideSequence(properties.getString("id", null), ca);
    043
    044      final Double[] hes = decodeCoordinateProperty(
    045        properties,
    046        "hes",
    047        val ->  val instanceof JsonNumber ? ((JsonNumber) val).doubleValue() : null,
    048        Double.class
    049      );
    050      final String[] imageIds = decodeCoordinateProperty(
    051        properties,
    052        "image_ids",
    053        val -> val instanceof JsonString ? ((JsonString) val).getString() : null,
    054        String.class
    055      );
    056      final LatLon[] geometry = decodeLatLons(json.getJsonObject("geometry"));
    057      final int sequenceLength = Math.min(Math.min(hes.length, imageIds.length), geometry.length);
    058      for (int i = 0; i < sequenceLength; i++) {
    059        if (hes[i] != null && imageIds[i] != null && geometry[i] != null) {
    060          final StreetsideImage img = new StreetsideImage(imageIds[i], geometry[i], hes[i]);
    061          result.add(img);
    062        }
    063      }
    064      if (result.getImages().isEmpty()) {
    065        result = null;
    066      }
    067    }
    068    return result;
    069  }
    070
    071  /**
    072   * Converts a {@link JsonArray} to a java array.
    073   * The conversion from {@link JsonValue} to a java type is done by the supplied function.
    074   * @param <T> object type
    075   * @param array the array to be converted
    076   * @param decodeValueFunction the function used for conversion from {@link JsonValue} to the desired type.
    077   * @param clazz the desired type that the elements of the resulting array should have
    078   * @return the supplied array converted from {@link JsonArray} to a java array of the supplied type, converted using
    079   *         the supplied function. Never <code>null</code>, in case of array==null, an array of length 0 is returned.
    080   */
    081  @SuppressWarnings("unchecked")
    082  private static <T> T[] decodeJsonArray(final JsonArray array, final Function<JsonValue, T> decodeValueFunction, final Class<T> clazz) {
    083    final T[] result;
    084    if (array == null) {
    085      result =  (T[]) Array.newInstance(clazz, 0);
    086    } else {
    087      result = (T[]) Array.newInstance(clazz, array.size());
    088      for (int i = 0; i < result.length; i++) {
    089        result[i] = decodeValueFunction.apply(array.get(i));
    090      }
    091    }
    092    return result;
    093  }
    094
    095  /**
    096   * Given the JSON object representing the `properties` of a sequence, this method converts you one attribute from the
    097   * `coordinateProperties` object to an array of appropriate type.
    098   *
    099   * For example this is used to convert the `image_keys` JSON array to a String[] array or the `cas` JSON array to a
    100   * Double[] array.
    101   * @param <T> object type
    102   * @param json the JSON object representing the `properties` of a sequence
    103   * @param key the key, which identifies the desired array inside the `coordinateProperties` object to be converted
    104   * @param decodeValueFunction a function that converts the {@link JsonValue}s in the JSON array to java objects of the
    105   *        desired type
    106   * @param clazz the {@link Class} object of the desired type, that the entries of the resulting array should have
    107   * @return the resulting array, when converting the desired entry of the `coordinateProperties`.
    108   *         Never <code>null</code>. If no `coordinateProperties` are set, or if the desired key is not set or is not
    109   *         an array, then an empty array of the desired type is returned.
    110   */
    111  @SuppressWarnings("unchecked")
    112  private static <T> T[] decodeCoordinateProperty(
    113    final JsonObject json, final String key, final Function<JsonValue, T> decodeValueFunction, final Class<T> clazz
    114  ) {
    115    final JsonValue coordinateProperties = json == null ? null : json.get("coordinateProperties");
    116    if (coordinateProperties instanceof JsonObject) {
    117      JsonValue valueArray = ((JsonObject) coordinateProperties).get(key);
    118      if (valueArray instanceof JsonArray) {
    119        return decodeJsonArray((JsonArray) valueArray, decodeValueFunction, clazz);
    120      }
    121    }
    122    return (T[]) Array.newInstance(clazz, 0);
    123  }
    124
    125  private static LatLon[] decodeLatLons(final JsonObject json) {
    126    final JsonValue coords = json == null ? null : json.get("coordinates");
    127    if (coords instanceof JsonArray && "LineString".equals(json.getString("type", null))) {
    128      final LatLon[] result = new LatLon[((JsonArray) coords).size()];
    129      for (int i = 0; i < ((JsonArray) coords).size(); i++) {
    130        final JsonValue coord = ((JsonArray) coords).get(i);
    131        if (coord instanceof JsonArray) {
    132          result[i] = JsonDecoder.decodeLatLon((JsonArray) coord);
    133        }
    134      }
    135      return result;
    136    }
    137    return new LatLon[0];
    138  }
    139}