001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.io.download; 003 004import java.io.BufferedInputStream; 005import java.io.IOException; 006import java.net.URL; 007import java.net.URLConnection; 008import java.text.MessageFormat; 009import java.util.ArrayList; 010import java.util.EnumSet; 011import java.util.List; 012import java.util.function.Function; 013 014import org.openstreetmap.josm.data.Bounds; 015import org.openstreetmap.josm.data.coor.LatLon; 016import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage; 017import org.openstreetmap.josm.plugins.streetside.StreetsideData; 018import org.openstreetmap.josm.plugins.streetside.StreetsideImage; 019import org.openstreetmap.josm.plugins.streetside.StreetsideSequence; 020import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils; 021import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties; 022import org.openstreetmap.josm.plugins.streetside.utils.StreetsideSequenceIdGenerator; 023import org.openstreetmap.josm.plugins.streetside.utils.StreetsideURL.APIv3; 024 025import com.fasterxml.jackson.core.JsonParseException; 026import com.fasterxml.jackson.core.JsonParser; 027import com.fasterxml.jackson.core.JsonToken; 028import com.fasterxml.jackson.databind.DeserializationFeature; 029import com.fasterxml.jackson.databind.JsonMappingException; 030import com.fasterxml.jackson.databind.ObjectMapper; 031import com.fasterxml.jackson.databind.node.ObjectNode; 032 033public final class SequenceDownloadRunnable extends BoundsDownloadRunnable { 034 035 private final StreetsideData data; 036 037 private static final Function<Bounds, URL> URL_GEN = APIv3::searchStreetsideImages; 038 039 public SequenceDownloadRunnable(final StreetsideData data, final Bounds bounds) { 040 super(bounds); 041 this.data = data; 042 } 043 044 @Override 045 public void run(final URLConnection con) throws IOException { 046 if (Thread.interrupted()) { 047 return; 048 } 049 050 StreetsideSequence seq = new StreetsideSequence(StreetsideSequenceIdGenerator.generateId()); 051 052 List<StreetsideImage> bubbleImages = new ArrayList<>(); 053 054 final long startTime = System.currentTimeMillis(); 055 056 ObjectMapper mapper = new ObjectMapper(); 057 // Creation of Jackson Object Mapper necessary for Silverlight 2.0 JSON Syntax parsing: 058 // (no double quotes in JSON on attribute names) 059 mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 060 061 // Allow unrecognized properties - won't break with addition of new attributes 062 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); 063 064 try { 065 JsonParser parser = mapper.getFactory().createParser(new BufferedInputStream(con.getInputStream())); 066 if(parser.nextToken() != JsonToken.START_ARRAY) { 067 parser.close(); 068 throw new IllegalStateException("Expected an array"); 069 } 070 071 StreetsideImage previous = null; 072 073 while (parser.nextToken() == JsonToken.START_OBJECT) { 074 // read everything from this START_OBJECT to the matching END_OBJECT 075 // and return it as a tree model ObjectNode 076 ObjectNode node = mapper.readTree(parser); 077 // Discard the first sequence ('enabled') - it does not contain bubble data 078 if (node.get("id") != null && node.get("la") != null && node.get("lo") != null) { 079 StreetsideImage image = new StreetsideImage(CubemapUtils.convertDecimal2Quaternary(node.path("id").asLong()), new LatLon(node.path("la").asDouble(), node.get("lo").asDouble()), node.get("he").asDouble()); 080 if(previous!=null) { 081 image.setPr(Long.parseLong(previous.getId())); 082 previous.setNe(Long.parseLong(image.getId())); 083 084 } 085 previous = image; 086 image.setAd(node.path("ad").asInt()); 087 image.setAl(node.path("al").asDouble()); 088 image.setBl(node.path("bl").asText()); 089 image.setMl(node.path("ml").asInt()); 090 image.setNbn(node.findValuesAsText("nbn")); 091 image.setNe(node.path("ne").asLong()); 092 image.setPbn(node.findValuesAsText("pbn")); 093 image.setPi(node.path("pi").asDouble()); 094 image.setPr(node.path("pr").asLong()); 095 image.setRo(node.path("ro").asDouble()); 096 097 // Add list of cubemap tile images to images 098 List<StreetsideImage> tiles = new ArrayList<>(); 099 100 EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> { 101 102 for (int i = 0; i < 4; i++) { 103 // Initialize four-tiled cubemap faces (four images per cube side with 18-length 104 // Quadkey) 105 if (!StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) { 106 StreetsideImage tile = new StreetsideImage(String.valueOf(image.getId() + Integer.valueOf(i))); 107 tiles.add(tile); 108 } 109 // Initialize four-tiled cubemap faces (four images per cub eside with 20-length 110 // Quadkey) 111 if (StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get()) { 112 for (int j = 0; j < 4; j++) { 113 StreetsideImage tile = new StreetsideImage( 114 String.valueOf( 115 image.getId() + face.getValue() + CubemapUtils.rowCol2StreetsideCellAddressMap 116 .get(String.valueOf(Integer.valueOf(i).toString() + Integer.valueOf(j).toString())) 117 )); 118 tiles.add(tile); 119 } 120 } 121 } 122 }); 123 124 bubbleImages.add(image); 125 logger.info("Added image with id <" + image.getId() + ">"); 126 if (StreetsideProperties.PREDOWNLOAD_CUBEMAPS.get()) { 127 StreetsideData.downloadSurroundingCubemaps(image); 128 } 129 } else { 130 logger.info(MessageFormat.format("Unparsable JSON node object: {0}",node.toString())); 131 } 132 } 133 134 parser.close(); 135 136 } catch (JsonParseException e) { 137 logger.error(MessageFormat.format("JSON parsing error occured during Streetside sequence download {0}",e.getMessage())); 138 } catch (JsonMappingException e) { 139 logger.error(MessageFormat.format("JSON mapping error occured during Streetside sequence download {0}",e.getMessage())); 140 } catch (IOException e) { 141 logger.error(MessageFormat.format("Input/output error occured during Streetside sequence download {0}",e.getMessage())); 142 } 143 144 /** Top Level Bubble Metadata in Streetside are bubble (aka images) not Sequences 145 * so a sequence needs to be created and have images added to it. If the distribution 146 * of Streetside images is non-sequential, the Mapillary "Walking Action" may behave 147 * unpredictably. 148 **/ 149 seq.add(bubbleImages); 150 151 if (StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.get()) { 152 for (StreetsideAbstractImage img : seq.getImages()) { 153 if (bounds.contains(img.getLatLon())) { 154 data.add(img); 155 } else { 156 seq.remove(img); 157 } 158 } 159 } else { 160 boolean sequenceCrossesThroughBounds = false; 161 for (int i = 0; i < seq.getImages().size() && !sequenceCrossesThroughBounds; i++) { 162 sequenceCrossesThroughBounds = bounds.contains(seq.getImages().get(i).getLatLon()); 163 } 164 if (sequenceCrossesThroughBounds) { 165 data.addAll(seq.getImages(), true); 166 } 167 } 168 169 final long endTime = System.currentTimeMillis(); 170 logger.info(MessageFormat.format("Sucessfully loaded {0} Microsoft Streetside images in {1} seconds.", seq.getImages().size(),(endTime-startTime)/1000)); 171 } 172 173 @Override 174 protected Function<Bounds, URL> getUrlGenerator() { 175 return URL_GEN; 176 } 177}