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