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}