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 JsonStreetsideSequencesDecoder {
023  private JsonStreetsideSequencesDecoder() {
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 && ca != null) {
042      result = new StreetsideSequence(properties.getString("id", null), ca);
043
044      final Double[] cas = 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(cas.length, imageIds.length), geometry.length);
058      for (int i = 0; i < sequenceLength; i++) {
059        if (cas[i] != null && imageIds[i] != null && geometry[i] != null) {
060          final StreetsideImage img = new StreetsideImage(imageIds[i], geometry[i], cas[i]);
061          result.add(img);
062        }
063      }
064      if (result.getImages().isEmpty()) {
065        result = null;
066      }
067    }
068    return result;
069  }
070
071  /**
072   * Parses a given {@link StreetsideImage} as a GeoJSON Feature into a {@link StreetsideSequence}.
073   * @param image the {@link StreetsideImage} to be parsed
074   * @return a {@link StreetsideSequence} that is parsed from the given {@link JsonObject}. If mandatory information is
075   *         missing from the JSON or it's not meeting the expecting format in another way, <code>null</code> will be
076   *         returned.
077   */
078  public static StreetsideImage decodeBubbleData(final StreetsideImage image) {
079    if (image == null) {
080      return null;
081    }
082    // Declare and instantiate new Streetside object to ensure proper setting of superclass attributes
083    StreetsideImage result = null;
084    if(image.getId() != null ) {
085      result = new StreetsideImage(image.getId(), new LatLon(image.getLa(), image.getLo()), 0.0);
086      result.setAl(image.getAl());
087      result.setRo(image.getRo());
088      result.setPi(image.getPi());
089      result.setHe(image.getHe());
090      result.setBl(image.getBl());
091      result.setMl(image.getMl());
092      result.setNe(image.getNe());
093      result.setPr(image.getPr());
094      result.setNbn(image.getNbn());
095      result.setPbn(image.getPbn());
096      result.setRn(image.getRn());
097      result.setCd(image.getCd());
098    }
099    return result;
100  }
101
102  /**
103   * Parses a given {@link JsonObject} as a GeoJSON Feature into a {@link StreetsideSequence}.
104   * @param json the {@link JsonObject} to be parsed
105   * @return a {@link StreetsideSequence} that is parsed from the given {@link JsonObject}. If mandatory information is
106   *         missing from the JSON or it's not meeting the expecting format in another way, <code>null</code> will be
107   *         returned.
108   */
109  public static StreetsideSequence decodeStreetsideSequence(final JsonObject json) {
110    if (json == null) {
111      return null;
112    }
113    StreetsideSequence result = null;
114
115    if (json.getString("id", null) != null && json.getString("la", null) != null && json.getString("lo", null) != null) {
116        result = new StreetsideSequence(json.getString("id", null), json.getJsonNumber("la").doubleValue(), json.getJsonNumber("lo").doubleValue(), json.getJsonNumber("cd").longValue());
117    }
118
119    return result;
120  }
121
122  /**
123   * Converts a {@link JsonArray} to a java array.
124   * The conversion from {@link JsonValue} to a java type is done by the supplied function.
125   * @param <T> object type
126   * @param array the array to be converted
127   * @param decodeValueFunction the function used for conversion from {@link JsonValue} to the desired type.
128   * @param clazz the desired type that the elements of the resulting array should have
129   * @return the supplied array converted from {@link JsonArray} to a java array of the supplied type, converted using
130   *         the supplied function. Never <code>null</code>, in case of array==null, an array of length 0 is returned.
131   */
132  @SuppressWarnings("unchecked")
133  private static <T> T[] decodeJsonArray(final JsonArray array, final Function<JsonValue, T> decodeValueFunction, final Class<T> clazz) {
134    final T[] result;
135    if (array == null) {
136      result =  (T[]) Array.newInstance(clazz, 0);
137    } else {
138      result = (T[]) Array.newInstance(clazz, array.size());
139      for (int i = 0; i < result.length; i++) {
140        result[i] = decodeValueFunction.apply(array.get(i));
141      }
142    }
143    return result;
144  }
145
146  /**
147   * Given the JSON object representing the `properties` of a sequence, this method converts you one attribute from the
148   * `coordinateProperties` object to an array of appropriate type.
149   *
150   * For example this is used to convert the `image_keys` JSON array to a String[] array or the `cas` JSON array to a
151   * Double[] array.
152   * @param <T> object type
153   * @param json the JSON object representing the `properties` of a sequence
154   * @param key the key, which identifies the desired array inside the `coordinateProperties` object to be converted
155   * @param decodeValueFunction a function that converts the {@link JsonValue}s in the JSON array to java objects of the
156   *        desired type
157   * @param clazz the {@link Class} object of the desired type, that the entries of the resulting array should have
158   * @return the resulting array, when converting the desired entry of the `coordinateProperties`.
159   *         Never <code>null</code>. If no `coordinateProperties` are set, or if the desired key is not set or is not
160   *         an array, then an empty array of the desired type is returned.
161   */
162  @SuppressWarnings("unchecked")
163  private static <T> T[] decodeCoordinateProperty(
164    final JsonObject json, final String key, final Function<JsonValue, T> decodeValueFunction, final Class<T> clazz
165  ) {
166    final JsonValue coordinateProperties = json == null ? null : json.get("coordinateProperties");
167    if (coordinateProperties instanceof JsonObject) {
168      JsonValue valueArray = ((JsonObject) coordinateProperties).get(key);
169      if (valueArray instanceof JsonArray) {
170        return decodeJsonArray((JsonArray) valueArray, decodeValueFunction, clazz);
171      }
172    }
173    return (T[]) Array.newInstance(clazz, 0);
174  }
175
176  private static LatLon[] decodeLatLons(final JsonObject json) {
177    final JsonValue coords = json == null ? null : json.get("coordinates");
178    if (coords instanceof JsonArray && "LineString".equals(json.getString("type", null))) {
179      final LatLon[] result = new LatLon[((JsonArray) coords).size()];
180      for (int i = 0; i < ((JsonArray) coords).size(); i++) {
181        final JsonValue coord = ((JsonArray) coords).get(i);
182        if (coord instanceof JsonArray) {
183          result[i] = JsonDecoder.decodeLatLon((JsonArray) coord);
184        }
185      }
186      return result;
187    }
188    return new LatLon[0];
189  }
190}