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}