Ticket #17177: 17177.4.patch

File 17177.4.patch, 170.9 KB (added by taylor.smock, 4 years ago)

Switch to using an OsmData class (VectorDataSet) along with Vector{Node,Way,Relation} in order to make styling easier (there is a significant performance hit with this patch).

  • new file resources/images/dialogs/add_mvt.svg

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/resources/images/dialogs/add_mvt.svg b/resources/images/dialogs/add_mvt.svg
    new file mode 100644
    - +  
     1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
     2<svg
     3   xmlns:dc="http://purl.org/dc/elements/1.1/"
     4   xmlns:cc="http://creativecommons.org/ns#"
     5   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     6   xmlns:svg="http://www.w3.org/2000/svg"
     7   xmlns="http://www.w3.org/2000/svg"
     8   xmlns:xlink="http://www.w3.org/1999/xlink"
     9   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
     10   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
     11   width="24"
     12   height="24"
     13   viewBox="0 0 24 24"
     14   id="svg2"
     15   version="1.1"
     16   inkscape:version="1.0.1 (c497b03c, 2020-09-10)"
     17   sodipodi:docname="add_mvt.svg">
     18  <defs
     19     id="defs4">
     20    <linearGradient
     21       gradientTransform="translate(4)"
     22       gradientUnits="userSpaceOnUse"
     23       y2="1049.3622"
     24       x2="12"
     25       y1="1041.3622"
     26       x1="4"
     27       id="linearGradient868"
     28       xlink:href="#linearGradient866"
     29       inkscape:collect="always" />
     30    <linearGradient
     31       id="linearGradient866"
     32       inkscape:collect="always">
     33      <stop
     34         id="stop862"
     35         offset="0"
     36         style="stop-color:#dfdfdf;stop-opacity:1" />
     37      <stop
     38         id="stop864"
     39         offset="1"
     40         style="stop-color:#949593;stop-opacity:1" />
     41    </linearGradient>
     42  </defs>
     43  <sodipodi:namedview
     44     id="base"
     45     pagecolor="#ffffff"
     46     bordercolor="#666666"
     47     borderopacity="1.0"
     48     inkscape:pageopacity="0"
     49     inkscape:pageshadow="2"
     50     inkscape:zoom="45.254834"
     51     inkscape:cx="11.376506"
     52     inkscape:cy="17.057298"
     53     inkscape:document-units="px"
     54     inkscape:current-layer="layer1"
     55     showgrid="true"
     56     units="px"
     57     inkscape:window-width="1920"
     58     inkscape:window-height="955"
     59     inkscape:window-x="0"
     60     inkscape:window-y="23"
     61     inkscape:window-maximized="1"
     62     viewbox-height="16"
     63     inkscape:document-rotation="0">
     64    <inkscape:grid
     65       type="xygrid"
     66       id="grid4136"
     67       originx="0"
     68       originy="0"
     69       spacingx="1"
     70       spacingy="1" />
     71  </sodipodi:namedview>
     72  <metadata
     73     id="metadata7">
     74    <rdf:RDF>
     75      <cc:Work
     76         rdf:about="">
     77        <dc:format>image/svg+xml</dc:format>
     78        <dc:type
     79           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
     80        <dc:title></dc:title>
     81        <cc:license
     82           rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
     83      </cc:Work>
     84      <cc:License
     85         rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
     86        <cc:permits
     87           rdf:resource="http://creativecommons.org/ns#Reproduction" />
     88        <cc:permits
     89           rdf:resource="http://creativecommons.org/ns#Distribution" />
     90        <cc:permits
     91           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
     92      </cc:License>
     93    </rdf:RDF>
     94  </metadata>
     95  <g
     96     inkscape:label="Layer 1"
     97     inkscape:groupmode="layer"
     98     id="layer1"
     99     transform="translate(0,-1037.3622)">
     100    <rect
     101       ry="0.48361239"
     102       y="1043.8622"
     103       x="5.5"
     104       height="3"
     105       width="13"
     106       id="rect833"
     107       style="opacity:1;fill:#c1c2c0;fill-opacity:1;fill-rule:nonzero;stroke:#555753;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.839909;stroke-opacity:1;paint-order:normal" />
     108    <rect
     109       transform="rotate(-90)"
     110       ry="0.48361239"
     111       y="10.5"
     112       x="-1051.8622"
     113       height="3"
     114       width="13"
     115       id="rect833-5"
     116       style="opacity:1;fill:#c1c2c0;fill-opacity:1;fill-rule:nonzero;stroke:#555753;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.839909;stroke-opacity:1;paint-order:normal" />
     117    <path
     118       inkscape:connector-curvature="0"
     119       id="path852"
     120       d="M 6.0000001,1044.3622 H 11 v -5 h 2 v 5 h 5 v 2 h -5 v 5 h -2 v -5 H 6.0000001 Z"
     121       style="fill:url(#linearGradient868);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
     122    <path
     123       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
     124       d="m 4.5,1060.3625 v -7.5948 l 2,4.3971 2,-4.3971 v 7.5948"
     125       id="path894"
     126       sodipodi:nodetypes="ccccc" />
     127    <path
     128       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
     129       d="m 17.5,1060.3622 v -8"
     130       id="path896" />
     131    <path
     132       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
     133       d="m 15,1052.8622 h 5"
     134       id="path898" />
     135    <text
     136       xml:space="preserve"
     137       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.3042px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.894202;stroke-miterlimit:4;stroke-dasharray:none"
     138       x="10.59868"
     139       y="898.41876"
     140       id="text854"
     141       transform="scale(0.84728029,1.180247)"><tspan
     142         sodipodi:role="line"
     143         id="tspan852"
     144         x="10.59868"
     145         y="898.41876"
     146         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.3042px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill-rule:nonzero;stroke-width:0.894202;stroke-miterlimit:4;stroke-dasharray:none">V</tspan></text>
     147  </g>
     148</svg>
  • src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
    a b  
    6161        /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/
    6262        WMS_ENDPOINT("wms_endpoint"),
    6363        /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/
    64         WMTS("wmts");
     64        WMTS("wmts"),
     65        /** MapBox Vector Tiles entry*/
     66        MVT("mvt");
    6567
    6668        private final String typeString;
    6769
     
    654656        defaultMaxZoom = 0;
    655657        defaultMinZoom = 0;
    656658        for (ImageryType type : ImageryType.values()) {
    657             Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)\\])?:(.*)").matcher(url);
     659            Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)])?:(.*)").matcher(url);
    658660            if (m.matches()) {
    659661                this.url = m.group(3);
    660662                this.sourceType = type;
     
    669671        }
    670672
    671673        if (serverProjections.isEmpty()) {
    672             Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase(Locale.ENGLISH));
     674            Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)}.*").matcher(url.toUpperCase(Locale.ENGLISH));
    673675            if (m.matches()) {
    674676                setServerProjections(Arrays.asList(m.group(1).split(",", -1)));
    675677            }
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
    a b  
    1010import java.nio.charset.StandardCharsets;
    1111import java.util.HashSet;
    1212import java.util.List;
     13import java.util.Locale;
    1314import java.util.Map;
    1415import java.util.Map.Entry;
    1516import java.util.Optional;
     
    3233import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
    3334import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
    3435import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     36import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     37import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
    3538import org.openstreetmap.josm.data.preferences.LongProperty;
    3639import org.openstreetmap.josm.tools.HttpClient;
    3740import org.openstreetmap.josm.tools.Logging;
     
    147150    private boolean isNotImage(Map<String, List<String>> headers, int statusCode) {
    148151        if (statusCode == 200 && headers.containsKey("Content-Type") && !headers.get("Content-Type").isEmpty()) {
    149152            String contentType = headers.get("Content-Type").stream().findAny().get();
    150             if (contentType != null && !contentType.startsWith("image")) {
     153            if (contentType != null && !contentType.startsWith("image") && !MVTFile.MIMETYPE.contains(contentType.toLowerCase(Locale.ROOT))) {
    151154                Logging.warn("Image not returned for tile: " + url + " content type was: " + contentType);
    152155                // not an image - do not store response in cache, so next time it will be queried again from the server
    153156                return true;
     
    321324            if (content.length > 0) {
    322325                try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
    323326                    tile.loadImage(in);
    324                     if (tile.getImage() == null) {
     327                    if ((!(tile instanceof VectorTile) && tile.getImage() == null)
     328                        || ((tile instanceof VectorTile) && !tile.isLoaded())) {
    325329                        String s = new String(content, StandardCharsets.UTF_8);
    326330                        Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
    327331                        if (m.matches()) {
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Command integers for Mapbox Vector Tiles
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum Command {
     10    /**
     11     * For {@link GeometryTypes#POINT}, each {@link #MoveTo} is a new point.
     12     * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #MoveTo} is a new geometry of the same type.
     13     */
     14    MoveTo((byte) 1, (byte) 2),
     15    /**
     16     * While not explicitly prohibited for {@link GeometryTypes#POINT}, it should be ignored.
     17     * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #LineTo} extends that geometry.
     18     */
     19    LineTo((byte) 2, (byte) 2),
     20    /**
     21     * This is only explicitly valid for {@link GeometryTypes#POLYGON}. It closes the {@link GeometryTypes#POLYGON}.
     22     */
     23    ClosePath((byte) 7, (byte) 0);
     24
     25    private final byte id;
     26    private final byte parameters;
     27
     28    Command(byte id, byte parameters) {
     29        this.id = id;
     30        this.parameters = parameters;
     31    }
     32
     33    /**
     34     * Get the command id
     35     * @return The id
     36     */
     37    public byte getId() {
     38        return this.id;
     39    }
     40
     41    /**
     42     * Get the number of parameters
     43     * @return The number of parameters
     44     */
     45    public byte getParameterNumber() {
     46        return this.parameters;
     47    }
     48}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.stream.Stream;
     5
     6/**
     7 * An indicator for a command to be executed
     8 * @author Taylor Smock
     9 * @since xxx
     10 */
     11public class CommandInteger {
     12    private final Command type;
     13    private final short[] parameters;
     14    private int added;
     15
     16    /**
     17     * Create a new command
     18     * @param command the command (treated as an unsigned int)
     19     */
     20    public CommandInteger(final int command) {
     21        // Technically, the int is unsigned, but it is easier to work with the long
     22        final long unsigned = Integer.toUnsignedLong(command);
     23        this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny()
     24                .orElseThrow(InvalidMapboxVectorTileException::new);
     25        // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned).
     26        // So we <i>cannot</i> lose anything.
     27        final int operationsInt = (int) (unsigned >> 3);
     28        this.parameters = new short[operationsInt * this.type.getParameterNumber()];
     29    }
     30
     31    /**
     32     * Add a parameter
     33     * @param parameterInteger The parameter to add (converted to {@link short}).
     34     */
     35    public void addParameter(Number parameterInteger) {
     36        this.parameters[added++] = parameterInteger.shortValue();
     37    }
     38
     39    /**
     40     * Get the operations for the command
     41     * @return The operations
     42     */
     43    public short[] getOperations() {
     44        return this.parameters;
     45    }
     46
     47    /**
     48     * Get the command type
     49     * @return the command type
     50     */
     51    public Command getType() {
     52        return this.type;
     53    }
     54
     55    /**
     56     * Get the expected parameter length
     57     * @return The expected parameter size
     58     */
     59    public boolean hasAllExpectedParameters() {
     60            return this.added >= this.parameters.length;
     61    }
     62}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.io.IOException;
     5import java.text.NumberFormat;
     6import java.util.ArrayList;
     7import java.util.List;
     8
     9import org.openstreetmap.josm.data.osm.TagMap;
     10import org.openstreetmap.josm.data.protobuf.ProtoBufPacked;
     11import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     12import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     13import org.openstreetmap.josm.tools.Utils;
     14
     15/**
     16 * A Feature for a {@link Layer}
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public class Feature {
     21    private static final byte ID_FIELD = 1;
     22    private static final byte TAG_FIELD = 2;
     23    private static final byte GEOMETRY_TYPE_FIELD = 3;
     24    private static final byte GEOMETRY_FIELD = 4;
     25    /** The geometry of the feature. Required. */
     26    private final List<CommandInteger> geometry = new ArrayList<>();
     27
     28    /** The geometry type of the feature. Required. */
     29    private final GeometryTypes geometryType;
     30
     31    /** The tags of the feature. Optional. */
     32    private TagMap tags;
     33
     34    /** The id of the feature. Optional. */
     35    // Technically, uint64
     36    private final long id;
     37
     38    /**
     39     * Create a new Feature
     40     * @param layer The layer the feature is part of (required for tags)
     41     * @param record The record to create the feature from
     42     * @throws IOException - if an IO error occurs
     43     */
     44    public Feature(Layer layer, ProtoBufRecord record) throws IOException {
     45        long tId = 0;
     46        GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
     47        String key = null;
     48        try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) {
     49            while (parser.hasNext()) {
     50                try (ProtoBufRecord next = new ProtoBufRecord(parser)) {
     51                    if (next.getField() == TAG_FIELD) {
     52                        if (tags == null) {
     53                            tags = new TagMap();
     54                        }
     55                        // This is packed in v1 and v2
     56                        ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     57                        for (Number number : packed.getArray()) {
     58                            key = parseTagValue(key, layer, number);
     59                        }
     60                    } else if (next.getField() == GEOMETRY_FIELD) {
     61                        // This is packed in v1 and v2
     62                        ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
     63                        CommandInteger currentCommand = null;
     64                        for (Number number : packed.getArray()) {
     65                            if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
     66                                currentCommand = null;
     67                            }
     68                            if (currentCommand == null) {
     69                                currentCommand = new CommandInteger(number.intValue());
     70                                this.geometry.add(currentCommand);
     71                            } else {
     72                                currentCommand.addParameter(ParameterInteger.decode(number.intValue()));
     73                            }
     74                        }
     75                        // TODO fallback to non-packed
     76                    } else if (next.getField() == GEOMETRY_TYPE_FIELD) {
     77                        geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()];
     78                    } else if (next.getField() == ID_FIELD) {
     79                        tId = next.asUnsignedVarInt().longValue();
     80                    }
     81                }
     82            }
     83        }
     84        this.id = tId;
     85        this.geometryType = geometryTypeTemp;
     86        record.close();
     87    }
     88
     89    /**
     90     * Parse a tag value
     91     * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key)
     92     * @param layer The layer with key/value information
     93     * @param number The number to get the value from
     94     * @return The new key (if {@code null}, then a value was parsed and added to tags)
     95     */
     96    private String parseTagValue(String key, Layer layer, Number number) {
     97        if (key == null) {
     98            key = layer.getKey(number.intValue());
     99        } else {
     100            Object value = layer.getValue(number.intValue());
     101            if (value instanceof Double || value instanceof Float) {
     102                // reset grouping if the instance is a singleton
     103                final NumberFormat numberFormat = NumberFormat.getNumberInstance();
     104                final boolean grouping = numberFormat.isGroupingUsed();
     105                try {
     106                    numberFormat.setGroupingUsed(false);
     107                    this.tags.put(key, numberFormat.format(value));
     108                } finally {
     109                    numberFormat.setGroupingUsed(grouping);
     110                }
     111            } else {
     112                this.tags.put(key, Utils.intern(value.toString()));
     113            }
     114            key = null;
     115        }
     116        return key;
     117    }
     118
     119    /**
     120     * Get the geometry instructions
     121     * @return The geometry
     122     */
     123    public List<CommandInteger> getGeometry() {
     124        return this.geometry;
     125    }
     126
     127    /**
     128     * Get the geometry type
     129     * @return The {@link GeometryTypes}
     130     */
     131    public GeometryTypes getGeometryType() {
     132        return this.geometryType;
     133    }
     134
     135    /**
     136     * Get the id of the object
     137     * @return The unique id in the layer, or 0.
     138     */
     139    public long getId() {
     140        return this.id;
     141    }
     142
     143    /**
     144     * Get the tags
     145     * @return A tag map
     146     */
     147    public TagMap getTags() {
     148        return this.tags;
     149    }
     150}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Shape;
     7import java.awt.geom.Area;
     8import java.awt.geom.Ellipse2D;
     9import java.awt.geom.Path2D;
     10import java.awt.geom.Point2D;
     11import java.util.Collection;
     12import java.util.Collections;
     13import java.util.HashSet;
     14import java.util.List;
     15
     16/**
     17 * A class to generate geometry for a vector tile
     18 * @author Taylor Smock
     19 * @since xxx
     20 */
     21public class Geometry {
     22    private static final byte CIRCLE_SIZE = 0;
     23    final Collection<Shape> shapes = new HashSet<>();
     24    private final Feature feature;
     25
     26    /**
     27     * Create a {@link Geometry} for a {@link Feature}
     28     * @param feature the {@link Feature} for the geometry
     29     */
     30    public Geometry(final Feature feature) {
     31        this.feature = feature;
     32        final GeometryTypes geometryType = this.feature.getGeometryType();
     33        final List<CommandInteger> commands = this.feature.getGeometry();
     34        final byte circleSize = CIRCLE_SIZE;
     35        if (geometryType == GeometryTypes.POINT) {
     36            for (CommandInteger command : commands) {
     37                final short[] operations = command.getOperations();
     38                // Each MoveTo command is a new point
     39                if (command.getType() == Command.MoveTo && operations.length % 2 == 0) {
     40                    for (int i = 0; i < operations.length / 2; i++) {
     41                        // move left/up by 1/2 circleSize, so that the circle is centered
     42                        shapes.add(new Ellipse2D.Float(operations[2 * i] - circleSize / 2f,
     43                                operations[2 * i + 1] - circleSize / 2f, circleSize, circleSize));
     44                    }
     45                } else {
     46                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     47                }
     48            }
     49        } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) {
     50            Path2D.Float line = null;
     51            Area area = null;
     52            // MVT uses delta encoding. Each feature starts at (0, 0).
     53            double x = 0;
     54            double y = 0;
     55            // Area is used to determine the inner/outer of a polygon
     56            double areaAreaSq = 0;
     57            for (CommandInteger command : commands) {
     58                final short[] operations = command.getOperations();
     59                // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior
     60                if (command.getType() == Command.MoveTo && operations.length == 2) {
     61                    areaAreaSq = 0;
     62                    x += operations[0];
     63                    y += operations[1];
     64                    line = new Path2D.Float();
     65                    line.moveTo(x, y);
     66                    shapes.add(line);
     67                } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) {
     68                    for (int i = 0; i < operations.length / 2; i++) {
     69                        final double lx = x;
     70                        final double ly = y;
     71                        x += operations[2 * i];
     72                        y += operations[2 * i + 1];
     73                        areaAreaSq += lx * y - x * ly;
     74                        line.lineTo(x, y);
     75                    }
     76                // ClosePath should only be used with Polygon geometry
     77                } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {
     78                    // ClosePath specifically does not change the cursor position
     79                    line.closePath();
     80                    line.setWindingRule(Path2D.WIND_NON_ZERO);
     81                    shapes.remove(line);
     82                    if (area == null) {
     83                        area = new Area(line);
     84                        shapes.add(area);
     85                    } else {
     86                        Area nArea = new Area(line);
     87                        // SonarLint thinks that this is never > 0. It can be.
     88                        if (areaAreaSq > 0) {
     89                            area.add(nArea);
     90                        } else {
     91                            area.exclusiveOr(nArea);
     92                        }
     93                    }
     94                } else {
     95                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     96                }
     97            }
     98        }
     99    }
     100
     101    /**
     102     * Get the feature for this geometry
     103     * @return The feature
     104     */
     105    public Feature getFeature() {
     106        return this.feature;
     107    }
     108
     109    /**
     110     * Get the shapes to draw this geometry with
     111     * @return A collection of shapes
     112     */
     113    public Collection<Shape> getShapes() {
     114        return Collections.unmodifiableCollection(this.shapes);
     115    }
     116}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Geometry types used by Mapbox Vector Tiles
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum GeometryTypes {
     10    /** May be ignored */
     11    UNKNOWN((byte) 0),
     12    /** May be a point or a multipoint geometry. Uses <i>only</i> {@link Command#MoveTo}. Multiple {@link Command#MoveTo}
     13     * indicates that it is a multi-point object. */
     14    POINT((byte) 1),
     15    /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */
     16    LINESTRING((byte) 2),
     17    /** May be a polygon or a multipolygon. Each ring uses a {@link Command#MoveTo}, one or more {@link Command#LineTo},
     18     * and one {@link Command#ClosePath} command. See {@link Ring}s. */
     19    POLYGON((byte) 3);
     20
     21    private final byte id;
     22    GeometryTypes(byte id) {
     23        this.id = id;
     24    }
     25
     26    /**
     27     * Get the id for the geometry type
     28     * @return The id
     29     */
     30    public byte getId() {
     31        return this.id;
     32    }
     33
     34    /**
     35     * Rings used by {@link GeometryTypes#POLYGON}
     36     * @author Taylor Smock
     37     */
     38    public enum Ring {
     39        /** A ring that goes in the clockwise direction */
     40        ExteriorRing,
     41        /** A ring that goes in the anti-clockwise direction */
     42        InteriorRing
     43    }
     44}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * Thrown when a mapbox vector tile does not match specifications.
     6 *
     7 * @author Taylor Smock
     8 * @since xxx
     9 */
     10public class InvalidMapboxVectorTileException extends RuntimeException {
     11    /**
     12     * Create a default {@link InvalidMapboxVectorTileException}.
     13     */
     14    public InvalidMapboxVectorTileException() {
     15        super();
     16    }
     17
     18    /**
     19     * Create a new {@link InvalidMapboxVectorTile} exception with a message
     20     * @param message The message
     21     */
     22    public InvalidMapboxVectorTileException(final String message) {
     23        super(message);
     24    }
     25}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.io.IOException;
     6import java.util.ArrayList;
     7import java.util.Arrays;
     8import java.util.Collection;
     9import java.util.Collections;
     10import java.util.HashSet;
     11import java.util.List;
     12import java.util.Map;
     13import java.util.Objects;
     14import java.util.function.Function;
     15import java.util.stream.Collectors;
     16
     17import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     18import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     19import org.openstreetmap.josm.tools.Logging;
     20
     21/**
     22 * A Mapbox Vector Tile Layer
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26public class Layer {
     27    private static final class ValueFields<T> {
     28        static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
     29        static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
     30        static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
     31        static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
     32        // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
     33        static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
     34        static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
     35        static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
     36
     37        public static final Collection<ValueFields<?>> MAPPERS = Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL);
     38
     39        private final byte field;
     40        private final Function<ProtoBufRecord, T> conversion;
     41        private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
     42            this.field = (byte) field;
     43            this.conversion = conversion;
     44        }
     45
     46        /**
     47         * Get the field identifier for the value
     48         * @return The identifier
     49         */
     50        public byte getField() {
     51            return this.field;
     52        }
     53
     54        /**
     55         * Convert a protobuf record to a value
     56         * @param protobufRecord The record to convert
     57         * @return the converted value
     58         */
     59        public T convertValue(ProtoBufRecord protobufRecord) {
     60            return this.conversion.apply(protobufRecord);
     61        }
     62    }
     63
     64    /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
     65    public static final byte LAYER_FIELD = 3;
     66    private static final byte VERSION_FIELD = 15;
     67    private static final byte NAME_FIELD = 1;
     68    private static final byte FEATURE_FIELD = 2;
     69    private static final byte KEY_FIELD = 3;
     70    private static final byte VALUE_FIELD = 4;
     71    private static final byte EXTENT_FIELD = 5;
     72    /** The default extent for a vector tile */
     73    static final int DEFAULT_EXTENT = 4096;
     74    private static final byte DEFAULT_VERSION = 1;
     75    /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
     76    private final byte version;
     77    /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
     78    private final String name;
     79
     80    /** The extent of the tile, typically 4096. Required. */
     81    private final int extent;
     82
     83    /** A list of unique keys. Order is important. Optional. */
     84    private final List<String> keyList = new ArrayList<>();
     85    /** A list of unique values. Order is important. Optional. */
     86    private final List<Object> valueList = new ArrayList<>();
     87    /** The actual features of this layer in this tile */
     88    private final List<Feature> featureCollection;
     89    /** The shapes to use to draw this layer */
     90    private final List<Geometry> geometryCollection;
     91
     92    /**
     93     * Create a layer from a collection of records
     94     * @param records The records to convert to a layer
     95     * @throws IOException - if an IO error occurs
     96     */
     97    public Layer(Collection<ProtoBufRecord> records) throws IOException {
     98        // Do the unique required fields first
     99        Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
     100        this.version = sorted.get((int) VERSION_FIELD).parallelStream().map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue)
     101                .findFirst().orElse(DEFAULT_VERSION);
     102        // Per spec, we cannot continue past this until we have checked the version number
     103        if (this.version != 1 && this.version != 2) {
     104            throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
     105        }
     106        this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
     107                .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
     108        this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt)
     109                .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
     110
     111        sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
     112                .forEachOrdered(this.keyList::add);
     113        sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes)
     114                .map(ProtoBufParser::new).map(parser1 -> {
     115                    try {
     116                        return new ProtoBufRecord(parser1);
     117                    } catch (IOException e) {
     118                        return null;
     119                    }
     120                })
     121                .filter(Objects::nonNull)
     122                .map(value -> ValueFields.MAPPERS.parallelStream()
     123                        .filter(v -> v.getField() == value.getField())
     124                        .map(v -> v.convertValue(value)).findFirst()
     125                        .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField()))))
     126                .forEachOrdered(this.valueList::add);
     127        Collection<IOException> exceptions = new HashSet<>(0);
     128        this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> {
     129            try {
     130                return new Feature(this, feature);
     131            } catch (IOException e) {
     132                exceptions.add(e);
     133            }
     134            return null;
     135        }).collect(Collectors.toList());
     136        this.geometryCollection = this.featureCollection.stream().map(Geometry::new).collect(Collectors.toList());
     137        if (!exceptions.isEmpty()) {
     138            throw exceptions.iterator().next();
     139        }
     140        // Cleanup bytes (for memory)
     141        for (ProtoBufRecord record : records) {
     142            try {
     143                record.close();
     144            } catch (Exception e) {
     145                Logging.error(e);
     146            }
     147        }
     148    }
     149
     150    /**
     151     * Create a new layer
     152     * @param bytes The bytes that the layer comes from
     153     * @throws IOException - if an IO error occurs
     154     */
     155    public Layer(byte[] bytes) throws IOException {
     156        this(new ProtoBufParser(bytes).allRecords());
     157    }
     158
     159    /**
     160     * Get the extent of the tile
     161     * @return The layer extent
     162     */
     163    public int getExtent() {
     164        return this.extent;
     165    }
     166
     167    /**
     168     * Get the feature on this layer
     169     * @return the features
     170     */
     171    public Collection<Feature> getFeatures() {
     172        return Collections.unmodifiableCollection(this.featureCollection);
     173    }
     174
     175    /**
     176     * Get the geometry for this layer
     177     * @return The geometry
     178     */
     179    public Collection<Geometry> getGeometry() {
     180        return Collections.unmodifiableCollection(this.geometryCollection);
     181    }
     182
     183    /**
     184     * Get a specified key
     185     * @param index The index in the key list
     186     * @return The actual key
     187     */
     188    public String getKey(int index) {
     189        return this.keyList.get(index);
     190    }
     191
     192    /**
     193     * Get the name of the layer
     194     * @return The layer name
     195     */
     196    public String getName() {
     197        return this.name;
     198    }
     199
     200    /**
     201     * Get a specified value
     202     * @param index The index in the value list
     203     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
     204     */
     205    public Object getValue(int index) {
     206        return this.valueList.get(index);
     207    }
     208
     209    /**
     210     * Get the MapBox Vector Tile version specification for this layer
     211     * @return The version of the MapBox Vector Tile specification
     212     */
     213    public byte getVersion() {
     214        return this.version;
     215    }
     216}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     9import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     13import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     14import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     15import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     16import org.openstreetmap.josm.data.imagery.TileJobOptions;
     17import org.openstreetmap.josm.data.preferences.IntegerProperty;
     18import org.openstreetmap.josm.tools.CheckParameterUtil;
     19
     20/**
     21 * A TileLoader class for MVT tiles
     22 * @author Taylor Smock
     23 * @since xxx
     24 */
     25public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader {
     26    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
     27    protected final TileLoaderListener listener;
     28    protected final TileJobOptions options;
     29    private static final IntegerProperty THREAD_LIMIT =
     30            new IntegerProperty("imagery.vector.mvtloader.maxjobs", TMSCachedTileLoader.THREAD_LIMIT.getDefaultValue());
     31    private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER =
     32            TMSCachedTileLoader.getNewThreadPoolExecutor("MVT-downloader-%d", THREAD_LIMIT.get());
     33
     34    /**
     35     * Constructor
     36     * @param listener          called when tile loading has finished
     37     * @param cache             of the cache
     38     * @param options           tile job options
     39     */
     40    public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
     41           TileJobOptions options) {
     42        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
     43        this.cache = cache;
     44        this.options = options;
     45        this.listener = listener;
     46    }
     47
     48    @Override
     49    public void clearCache(TileSource source) {
     50        this.cache.remove(source.getName() + ':');
     51    }
     52
     53    @Override
     54    public TileJob createTileLoaderJob(Tile tile) {
     55        return new MapBoxVectorCachedTileLoaderJob(
     56                listener,
     57                tile,
     58                cache,
     59                options,
     60                getDownloadExecutor());
     61    }
     62
     63    @Override
     64    public void cancelOutstandingTasks() {
     65        final ThreadPoolExecutor executor = getDownloadExecutor();
     66        executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance)
     67                .map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation);
     68    }
     69
     70    @Override
     71    public boolean hasOutstandingTasks() {
     72        return getDownloadExecutor().getTaskCount() > getDownloadExecutor().getCompletedTaskCount();
     73    }
     74
     75    private ThreadPoolExecutor getDownloadExecutor() {
     76        return DEFAULT_DOWNLOAD_JOB_DISPATCHER;
     77    }
     78}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     9import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     10import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
     11import org.openstreetmap.josm.data.imagery.TileJobOptions;
     12
     13/**
     14 * Bridge to JCS cache for MVT tiles
     15 * @author Taylor Smock
     16 * @since xxx
     17 */
     18public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
     19
     20    public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
     21            ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
     22            ThreadPoolExecutor downloadExecutor) {
     23        super(listener, tile, cache, options, downloadExecutor);
     24    }
     25}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import org.openstreetmap.josm.data.imagery.ImageryInfo;
     5import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
     6import org.openstreetmap.josm.data.projection.Projection;
     7
     8/**
     9 * Tile Source handling for Mapbox Vector Tile sources
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
     14    public MapboxVectorTileSource(ImageryInfo info) {
     15        super(info);
     16    }
     17}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.Arrays;
     5import java.util.List;
     6
     7/**
     8 * Items that MAY be used to figure out if a file or server response MAY BE a Mapbox Vector Tile
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public final class MVTFile {
     13    /**
     14     * Extensions for Mapbox Vector Tiles.
     15     * This is a SHOULD, <i>not</i> a MUST.
     16     */
     17    public static final List<String> EXTENSION = Arrays.asList("mvt");
     18
     19    /**
     20     * mimetypes for Mapbox Vector Tiles
     21     * This is a SHOULD, <i>not</i> a MUST.
     22     */
     23    public static final List<String> MIMETYPE = Arrays.asList("application/vnd.mapbox-vector-tile");
     24
     25    /**
     26     * The default projection. This is Web Mercator, per specification.
     27     */
     28    public static final String DEFAULT_PROJECTION = "EPSG:3857";
     29
     30    private MVTFile() {
     31        // Hide the constructor
     32    }
     33}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.awt.Color;
     5import java.awt.Graphics;
     6import java.awt.Graphics2D;
     7import java.awt.Shape;
     8import java.awt.Stroke;
     9import java.awt.geom.AffineTransform;
     10import java.awt.geom.Area;
     11import java.awt.geom.Ellipse2D;
     12import java.awt.geom.Path2D;
     13import java.awt.image.BufferedImage;
     14import java.awt.image.ImageObserver;
     15import java.io.IOException;
     16import java.io.InputStream;
     17import java.util.ArrayList;
     18import java.util.Collection;
     19import java.util.HashSet;
     20import java.util.List;
     21import java.util.Objects;
     22import java.util.function.UnaryOperator;
     23import java.util.stream.Collectors;
     24
     25import org.openstreetmap.gui.jmapviewer.Tile;
     26import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     27import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     28import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     29import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     30import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     31import org.openstreetmap.josm.data.vector.VectorDataSet;
     32import org.openstreetmap.josm.tools.ListenerList;
     33import org.openstreetmap.josm.tools.Logging;
     34
     35/**
     36 * A class for MapBox Vector Tiles
     37 * @author Taylor Smock
     38 * @since xxx
     39 */
     40public class MVTTile extends Tile implements VectorTile {
     41    private Collection<Layer> layers;
     42    private int extent = Layer.DEFAULT_EXTENT;
     43    private final ListenerList<TileListener> listenerList = ListenerList.create();
     44    private LayerShower layerShower;
     45
     46    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
     47        super(source, xtile, ytile, zoom);
     48    }
     49
     50    @Override
     51    public void paint(final Graphics g, final int x, final int y) {
     52        this.paint(g, x, y, 256, 256);
     53    }
     54
     55    @Override
     56    public void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer) {
     57        if (!(g instanceof Graphics2D) || this.layers == null) {
     58            if (getImage() != null) {
     59                g.drawImage(image, x, y, width, height, observer);
     60            }
     61            return;
     62        }
     63        final Graphics2D graphics = (Graphics2D) g;
     64        graphics.setColor(Color.GREEN);
     65        final AffineTransform originalTransform = graphics.getTransform();
     66        final Stroke originalStroke = graphics.getStroke();
     67        try {
     68            graphics.translate(x, y);
     69            // TODO figure out HiDPI (maybe GuiSizesHelper?)
     70            // 131072 seems to be the magic number for my screens. This needs to be investigated more.
     71            final double scale = width / (double) (32768);
     72            // The scaleTransform is separate to avoid wide lines at high zoom (e.g., when vector tiles go to z14, but
     73            // we are currently at z20, the graphics.scale function makes everything big. Unfortunately, this creates
     74            // a new shape object.
     75            final UnaryOperator<Shape> scaleShape;
     76            if (scale > 1) {
     77                final AffineTransform scaleTransform = AffineTransform.getScaleInstance(scale, scale);
     78                scaleShape = scaleTransform::createTransformedShape;
     79            } else {
     80                // Cannot use Objects.identity() since it isn't a UnaryOperator
     81                scaleShape = s -> s;
     82                graphics.scale(scale, scale);
     83            }
     84            final Color transparentYellow = new Color(Color.YELLOW.getRed(), Color.YELLOW.getGreen(), Color.YELLOW.getBlue(), 120);
     85            Collection<Layer> layersToShow = new ArrayList<>();
     86            if (this.layerShower != null) {
     87                this.layerShower.layersToShow().stream().map(layer -> this.layers.stream().filter(l -> Objects.equals(layer, l.getName())).findAny().orElse(null)).filter(Objects::nonNull).forEach(layersToShow::add);
     88            } else {
     89                layersToShow.addAll(this.layers);
     90            }
     91            for (Layer layer : layersToShow) {
     92                layer.getGeometry().forEach(shapes -> {
     93                    for (Shape shape : shapes.getShapes()) {
     94                        final Shape scaledShape = scaleShape.apply(shape);
     95                        if (shape instanceof Ellipse2D) {
     96                            graphics.setColor(Color.GREEN);
     97                        } else if (shape instanceof Path2D) {
     98                            graphics.setColor(Color.RED);
     99                        } else if (shape instanceof Area) {
     100                            graphics.setColor(transparentYellow);
     101                            graphics.fill(scaledShape);
     102                            graphics.setColor(Color.YELLOW);
     103                        }
     104                        graphics.draw(scaledShape);
     105                    }
     106                });
     107            }
     108        } finally {
     109            graphics.setTransform(originalTransform);
     110            graphics.setStroke(originalStroke);
     111        }
     112    }
     113
     114    @Override
     115    public void loadImage(final InputStream inputStream) throws IOException {
     116        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
     117            this.initLoading();
     118            ProtoBufParser parser = new ProtoBufParser(inputStream);
     119            Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
     120            this.layers = new HashSet<>();
     121            this.layers = protoBufRecords.stream().map(record -> {
     122                Layer mvtLayer = null;
     123                if (record.getField() == Layer.LAYER_FIELD) {
     124                    try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) {
     125                        mvtLayer = new Layer(tParser.allRecords());
     126                    } catch (IOException e) {
     127                        Logging.error(e);
     128                    } finally {
     129                        // Cleanup bytes
     130                        record.close();
     131                    }
     132                }
     133                return mvtLayer;
     134            }).collect(Collectors.toCollection(HashSet::new));
     135            this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
     136            BufferedImage bufferedImage = new BufferedImage(this.extent, this.extent, BufferedImage.TYPE_4BYTE_ABGR);
     137            Graphics2D graphics = bufferedImage.createGraphics();
     138            this.paint(graphics, 0, 0);
     139
     140            // TODO figure out a better way to free memory
     141            final int maxSize = 256;
     142            final BufferedImage resized = new BufferedImage(maxSize, maxSize, bufferedImage.getType());
     143            resized.getGraphics().drawImage(bufferedImage, 0, 0, resized.getWidth(), resized.getHeight(), null);
     144            this.image = resized;
     145            this.finishLoading();
     146            Collection<TileListener> fired = new ArrayList<>();
     147            this.listenerList.fireEvent(event -> {
     148                event.finishedLoading(this);
     149                fired.add(event);
     150            });
     151            // We shouldn't keep object references around once we no longer need them
     152            fired.forEach(this.listenerList::removeListener);
     153        }
     154    }
     155
     156    @Override
     157    public Collection<Layer> getLayers() {
     158        return this.layers;
     159    }
     160
     161    @Override
     162    public int getExtent() {
     163        return this.extent;
     164    }
     165
     166    /**
     167     * Set the object that will determine what layers are shown
     168     * @param shower The class that will determine the layers to paint
     169     */
     170    public void setLayerShower(LayerShower shower) {
     171        this.layerShower = shower;
     172    }
     173
     174
     175    /**
     176     * A class that can be notified that a tile has finished loading
     177     * @author Taylor Smock
     178     *
     179     */
     180    public static interface TileListener {
     181        /**
     182         * Called when the MVTTile is finished loading
     183         * @param tile The tile that finished loading
     184         */
     185        void finishedLoading(final MVTTile tile);
     186    }
     187
     188    /**
     189     * A class used to set the layers that an MVTTile will show.
     190     * @author Taylor Smock
     191     *
     192     */
     193    public static interface LayerShower {
     194        /**
     195         * Get a list of layers to show
     196         * @return A list of layer names
     197         */
     198        List<String> layersToShow();
     199    }
     200
     201    /**
     202     * Add a tile loader finisher listener
     203     * @param listener The listener to add
     204     */
     205    public void addTileLoaderFinisher(TileListener listener) {
     206        // Add as weak listeners since we don't want to keep unnecessary references.
     207        this.listenerList.addWeakListener(listener);
     208    }
     209}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4/**
     5 * The parameters that follow the {@link CommandInteger}.
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public final class ParameterInteger {
     10    private ParameterInteger() {
     11        // Hide constructor
     12    }
     13
     14    /**
     15     * Get the value for this ParameterInteger
     16     * @param value The zig-zag and delta encoded value to decode
     17     * @return The decoded integer value
     18     */
     19    public static int decode(int value) {
     20        return ((value >> 1) ^ -(value & 1));
     21    }
     22}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile;
     3
     4import java.awt.Graphics;
     5import java.awt.image.ImageObserver;
     6import java.util.Collection;
     7
     8import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     9import org.openstreetmap.josm.data.vector.VectorDataSet;
     10
     11/**
     12 * An interface that is used to draw vector tiles, instead of using images
     13 * @author Taylor Smock
     14 * @since xxx
     15 */
     16public interface VectorTile {
     17    /**
     18     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     19     * position <code>x</code>/<code>y</code>.
     20     *
     21     * @param g the Graphics object
     22     * @param x x-coordinate in <code>g</code>
     23     * @param y y-coordinate in <code>g</code>
     24     */
     25    @Deprecated
     26    void paint(Graphics g, int x, int y);
     27
     28    /**
     29     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     30     * position <code>x</code>/<code>y</code>.
     31     *  @param g the Graphics object
     32     * @param x x-coordinate in <code>g</code>
     33     * @param y y-coordinate in <code>g</code>
     34     * @param width width that tile should have
     35     * @param height height that tile should have
     36     * @param observer The paint observer. May be {@code null}.
     37     * @param zoom The current zoom level
     38     */
     39    @Deprecated
     40    void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer);
     41
     42    /**
     43     * Get the layers for this vector tile
     44     * @return A collection of layers
     45     */
     46    Collection<Layer> getLayers();
     47
     48    /**
     49     * Get the extent of the tile (in pixels)
     50     * @return The tile extent (pixels)
     51     */
     52    int getExtent();
     53}
  • new file src/org/openstreetmap/josm/data/osm/IWaySegment.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/IWaySegment.java b/src/org/openstreetmap/josm/data/osm/IWaySegment.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm;
     3
     4  import java.awt.geom.Line2D;
     5  import java.util.Arrays;
     6  import java.util.Objects;
     7
     8  import org.openstreetmap.josm.tools.Logging;
     9
     10/**
     11 * A segment consisting of 2 consecutive nodes out of a way.
     12 */
     13public class IWaySegment<N extends INode, W extends IWay<N>> implements Comparable<IWaySegment> {
     14
     15    /**
     16     * The way.
     17     */
     18    public final W way;
     19
     20    /**
     21     * The index of one of the 2 nodes in the way.  The other node has the
     22     * index <code>lowerIndex + 1</code>.
     23     */
     24    public final int lowerIndex;
     25
     26    /**
     27     * Constructs a new {@code IWaySegment}.
     28     * @param w The way
     29     * @param i The node lower index
     30     * @throws IllegalArgumentException in case of invalid index
     31     */
     32    public IWaySegment(W w, int i) {
     33        way = w;
     34        lowerIndex = i;
     35        if (i < 0 || i >= w.getNodesCount() - 1) {
     36            throw new IllegalArgumentException(toString());
     37        }
     38    }
     39
     40    /**
     41     * Returns the first node of the way segment.
     42     * @return the first node
     43     */
     44    public N getFirstNode() {
     45        return way.getNode(lowerIndex);
     46    }
     47
     48    /**
     49     * Returns the second (last) node of the way segment.
     50     * @return the second node
     51     */
     52    public N getSecondNode() {
     53        return way.getNode(lowerIndex + 1);
     54    }
     55
     56    /**
     57     * Determines and returns the way segment for the given way and node pair.
     58     * @param way way
     59     * @param first first node
     60     * @param second second node
     61     * @return way segment
     62     * @throws IllegalArgumentException if the node pair is not part of way
     63     */
     64    public static <N extends INode, W extends IWay<N>> IWaySegment<N, W> forNodePair(W way, N first, N second) {
     65        int endIndex = way.getNodesCount() - 1;
     66        while (endIndex > 0) {
     67            final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first);
     68            if (second.equals(way.getNode(indexOfFirst + 1))) {
     69                return new IWaySegment<>(way, indexOfFirst);
     70            }
     71            endIndex--;
     72        }
     73        throw new IllegalArgumentException("Node pair is not part of way!");
     74    }
     75
     76    /**
     77     * Returns this way segment as complete way.
     78     * @return the way segment as {@code Way}
     79     */
     80    public W toWay() throws IllegalAccessException, InstantiationException {
     81        W w = (W) this.way.getClass().newInstance();
     82        w.setNodes(Arrays.asList(getFirstNode(), getSecondNode()));
     83        return w;
     84    }
     85
     86    @Override
     87    public boolean equals(Object o) {
     88        if (this == o) return true;
     89        if (o == null || getClass() != o.getClass()) return false;
     90        IWaySegment that = (IWaySegment) o;
     91        return lowerIndex == that.lowerIndex &&
     92          Objects.equals(way, that.way);
     93    }
     94
     95    @Override
     96    public int hashCode() {
     97        return Objects.hash(way, lowerIndex);
     98    }
     99
     100    @Override
     101    public int compareTo(IWaySegment o) {
     102        final W thisWay;
     103        final IWay otherWay;
     104        try {
     105            thisWay = toWay();
     106            otherWay = o == null ? null : o.toWay();
     107        } catch (IllegalAccessException | InstantiationException e) {
     108            Logging.error(e);
     109            return -1;
     110        }
     111        return o == null ? -1 : (equals(o) ? 0 : thisWay.compareTo(otherWay));
     112    }
     113
     114    /**
     115     * Checks whether this segment crosses other segment
     116     *
     117     * @param s2 The other segment
     118     * @return true if both segments crosses
     119     */
     120    public boolean intersects(IWaySegment s2) {
     121        if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||
     122          getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))
     123            return false;
     124
     125        return Line2D.linesIntersect(
     126          getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),
     127          getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),
     128          s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),
     129          s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
     130    }
     131
     132    /**
     133     * Checks whether this segment and another way segment share the same points
     134     * @param s2 The other segment
     135     * @return true if other way segment is the same or reverse
     136     */
     137    public boolean isSimilar(IWaySegment s2) {
     138        return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
     139          || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));
     140    }
     141
     142    @Override
     143    public String toString() {
     144        return "IWaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
     145    }
     146}
  • src/org/openstreetmap/josm/data/osm/WaySegment.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/WaySegment.java b/src/org/openstreetmap/josm/data/osm/WaySegment.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.osm;
    33
    4 import java.awt.geom.Line2D;
    5 import java.util.Objects;
    6 
    74/**
    85 * A segment consisting of 2 consecutive nodes out of a way.
    96 */
    10 public final class WaySegment implements Comparable<WaySegment> {
    11 
    12     /**
    13      * The way.
    14      */
    15     public final Way way;
    16 
    17     /**
    18      * The index of one of the 2 nodes in the way.  The other node has the
    19      * index <code>lowerIndex + 1</code>.
    20      */
    21     public final int lowerIndex;
     7public final class WaySegment extends IWaySegment<Node, Way> {
    228
    239    /**
    24      * Constructs a new {@code WaySegment}.
    25      * @param w The way
    26      * @param i The node lower index
     10     * Constructs a new {@code IWaySegment}.
     11     *
     12     * @param way The way
     13     * @param i   The node lower index
    2714     * @throws IllegalArgumentException in case of invalid index
    2815     */
    29     public WaySegment(Way w, int i) {
    30         way = w;
    31         lowerIndex = i;
    32         if (i < 0 || i >= w.getNodesCount() - 1) {
    33             throw new IllegalArgumentException(toString());
    34         }
    35     }
    36 
    37     /**
    38      * Returns the first node of the way segment.
    39      * @return the first node
    40      */
    41     public Node getFirstNode() {
    42         return way.getNode(lowerIndex);
     16    public WaySegment(Way way, int i) {
     17        super(way, i);
    4318    }
    4419
    4520    /**
    46      * Returns the second (last) node of the way segment.
    47      * @return the second node
    48      */
    49     public Node getSecondNode() {
    50         return way.getNode(lowerIndex + 1);
    51     }
    52 
    53     /**
    54      * Determines and returns the way segment for the given way and node pair.
     21     * Determines and returns the way segment for the given way and node pair. You should prefer
     22     * {@link IWaySegment#forNodePair(IWay, INode, INode)} whenever possible.
     23     *
    5524     * @param way way
    5625     * @param first first node
    5726     * @param second second node
     
    7443     * Returns this way segment as complete way.
    7544     * @return the way segment as {@code Way}
    7645     */
     46    @Override
    7747    public Way toWay() {
    7848        Way w = new Way();
    7949        w.addNode(getFirstNode());
     
    8151        return w;
    8252    }
    8353
    84     @Override
    85     public boolean equals(Object o) {
    86         if (this == o) return true;
    87         if (o == null || getClass() != o.getClass()) return false;
    88         WaySegment that = (WaySegment) o;
    89         return lowerIndex == that.lowerIndex &&
    90                 Objects.equals(way, that.way);
    91     }
    92 
    93     @Override
    94     public int hashCode() {
    95         return Objects.hash(way, lowerIndex);
    96     }
    97 
    98     @Override
    99     public int compareTo(WaySegment o) {
    100         return o == null ? -1 : (equals(o) ? 0 : toWay().compareTo(o.toWay()));
    101     }
    102 
    103     /**
    104      * Checks whether this segment crosses other segment
    105      *
    106      * @param s2 The other segment
    107      * @return true if both segments crosses
    108      */
    109     public boolean intersects(WaySegment s2) {
    110         if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||
    111                 getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))
    112             return false;
    113 
    114         return Line2D.linesIntersect(
    115                 getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),
    116                 getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),
    117                 s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),
    118                 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
    119     }
    120 
    121     /**
    122      * Checks whether this segment and another way segment share the same points
    123      * @param s2 The other segment
    124      * @return true if other way segment is the same or reverse
    125      */
    126     public boolean isSimilar(WaySegment s2) {
    127         return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
    128             || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));
    129     }
    130 
    13154    @Override
    13255    public String toString() {
    13356        return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7/**
     8 * Parse packed values (only numerical values)
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public class ProtoBufPacked {
     13    private final byte[] bytes;
     14    private int location;
     15    private final Number[] numbers;
     16    /**
     17     * Create a new ProtoBufPacked object
     18     * @param bytes The packed bytes
     19     */
     20    public ProtoBufPacked(byte[] bytes) {
     21        this.location = 0;
     22        this.bytes = bytes;
     23        List<Number> numbersT = new ArrayList<>();
     24        while (this.location < bytes.length) {
     25            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
     26        }
     27
     28        this.numbers = new Number[numbersT.size()];
     29        for (int i = 0; i < numbersT.size(); i++) {
     30            this.numbers[i] = numbersT.get(i);
     31        }
     32    }
     33
     34    /**
     35     * The number of expected values
     36     * @return The expected values
     37     */
     38    public int size() {
     39        return this.numbers.length;
     40    }
     41
     42    /**
     43     * Get the parsed number array
     44     * @return The number array
     45     */
     46    public Number[] getArray() {
     47        return this.numbers;
     48    }
     49
     50    private byte[] nextVarInt() {
     51        List<Byte> byteList = new ArrayList<>();
     52        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     53            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     54            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     55        }
     56        // The last byte doesn't drop the most significant bit
     57        byteList.add(this.bytes[this.location++]);
     58        byte[] byteArray = new byte[byteList.size()];
     59        for (int i = 0; i < byteList.size(); i++) {
     60            byteArray[i] = byteList.get(i);
     61        }
     62
     63        return byteArray;
     64    }
     65}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.BufferedInputStream;
     5import java.io.ByteArrayInputStream;
     6import java.io.IOException;
     7import java.io.InputStream;
     8import java.util.ArrayList;
     9import java.util.Collection;
     10import java.util.List;
     11
     12import org.openstreetmap.josm.tools.Logging;
     13
     14/**
     15 * A basic Protobuf parser
     16 * @author Taylor Smock
     17 * @since xxx
     18 */
     19public class ProtoBufParser implements AutoCloseable {
     20    /**
     21     * Used to get the most significant byte
     22     */
     23    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     24    /** The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints) */
     25    public static final byte BYTE_SIZE = 8;
     26    /** The byte size for var ints (since the first byte is just an indicator for if the var int is done) */
     27    public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1;
     28    // TODO switch to a better parser
     29    private final InputStream inputStream;
     30    /**
     31     * Create a new parser
     32     * @param bytes The bytes to parse
     33     */
     34    public ProtoBufParser(byte[] bytes) {
     35        this(new ByteArrayInputStream(bytes));
     36    }
     37
     38    /**
     39     * Create a new parser
     40     * @param inputStream The InputStream (will be fully read at this time)
     41     */
     42    public ProtoBufParser(InputStream inputStream) {
     43        if (inputStream.markSupported()) {
     44            this.inputStream = inputStream;
     45        } else {
     46            this.inputStream = new BufferedInputStream(inputStream);
     47        }
     48    }
     49
     50    @Override
     51    public void close() {
     52        try {
     53            this.inputStream.close();
     54        } catch (IOException e) {
     55            Logging.error(e);
     56        }
     57    }
     58
     59    /**
     60     * Get the "next" WireType
     61     * @return {@link WireType} expected
     62     * @throws IOException - if an IO error occurs
     63     */
     64    public WireType next() throws IOException {
     65        this.inputStream.mark(16);
     66        try {
     67            return WireType.values()[this.inputStream.read() << 3];
     68        } finally {
     69            this.inputStream.reset();
     70        }
     71    }
     72
     73    /**
     74     * Get the next byte
     75     * @return The next byte
     76     * @throws IOException - if an IO error occurs
     77     */
     78    public int nextByte() throws IOException {
     79        return this.inputStream.read();
     80    }
     81
     82    /**
     83     * Check if there is more data to read
     84     * @return {@code true} if there is more data to read
     85     * @throws IOException - if an IO error occurs
     86     */
     87    public boolean hasNext() throws IOException {
     88        return this.inputStream.available() > 0;
     89    }
     90
     91    /**
     92     * Get the next var int ({@code WireType#VARINT})
     93     * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     94     * @throws IOException - if an IO error occurs
     95     */
     96    public byte[] nextVarInt() throws IOException {
     97        List<Byte> byteList = new ArrayList<>();
     98        int currentByte = this.nextByte();
     99        while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE) {
     100            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     101            byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE));
     102            currentByte = this.nextByte();
     103        }
     104        // The last byte doesn't drop the most significant bit
     105        byteList.add((byte) currentByte);
     106        byte[] byteArray = new byte[byteList.size()];
     107        for (int i = 0; i < byteList.size(); i++) {
     108            byteArray[i] = byteList.get(i);
     109        }
     110
     111        return byteArray;
     112    }
     113
     114    /**
     115     * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
     116     * @return a byte array of the next 32 bits (4 bytes)
     117     * @throws IOException - if an IO error occurs
     118     */
     119    public byte[] nextFixed32() throws IOException {
     120        // 4 bytes == 32 bits
     121        return readNextBytes(4);
     122    }
     123
     124    /**
     125     * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     126     * @return a byte array of the next 64 bits (8 bytes)
     127     * @throws IOException - if an IO error occurs
     128     */
     129    public byte[] nextFixed64() throws IOException {
     130        // 8 bytes == 64 bits
     131        return readNextBytes(8);
     132    }
     133
     134    /**
     135     * Read an arbitrary number of bytes
     136     * @param size The number of bytes to read
     137     * @return a byte array of the specified size, filled with bytes read (unsigned)
     138     * @throws IOException - if an IO error occurs
     139     */
     140    private byte[] readNextBytes(int size) throws IOException {
     141        byte[] bytesRead = new byte[size];
     142        for (int i = 0; i < bytesRead.length; i++) {
     143            bytesRead[i] = (byte) this.nextByte();
     144        }
     145        return bytesRead;
     146    }
     147
     148    /**
     149     * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
     150     * @return The next length delimited message
     151     * @throws IOException - if an IO error occurs
     152     */
     153    public byte[] nextLengthDelimited() throws IOException {
     154        int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue();
     155        return readNextBytes(length);
     156    }
     157
     158    /**
     159     * Convert a byte array to a number (little endian)
     160     * @param bytes The bytes to convert
     161     * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8.
     162     * @return An appropriate {@link Number} class.
     163     */
     164    public static Number convertByteArray(byte[] bytes, byte byteSize) {
     165        long number = 0;
     166        for (int i = 0; i < bytes.length; i++) {
     167            // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues
     168            number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i);
     169        }
     170        return convertLong(number);
     171    }
     172
     173    /**
     174     * Convert a long to an appropriate {@link Number} class
     175     * @param number The long to convert
     176     * @return A {@link Number}
     177     */
     178    public static Number convertLong(long number) {
     179        // TODO deal with booleans
     180        if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
     181            return (byte) number;
     182        } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
     183            return (short) number;
     184        } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
     185            return (int) number;
     186        }
     187        return number;
     188    }
     189
     190    /**
     191     * Read all records
     192     * @return A collection of all records
     193     * @throws IOException - if an IO error occurs
     194     */
     195    public Collection<ProtoBufRecord> allRecords() throws IOException {
     196        Collection<ProtoBufRecord> records = new ArrayList<>();
     197        while (this.hasNext()) {
     198            records.add(new ProtoBufRecord(this));
     199        }
     200        return records;
     201    }
     202
     203    /**
     204     * Decode a zig-zag encoded value
     205     * @param signed The value to decode
     206     * @return The decoded value
     207     */
     208    public static Number decodeZigZag(Number signed) {
     209        final long value = signed.longValue();
     210        return convertLong((value >> 1) ^ (-(value & 1)));
     211    }
     212
     213    /**
     214     * Encode a number to a zig-zag encode value
     215     * @param signed The number to encode
     216     * @return The encoded value
     217     */
     218    public static Number encodeZigZag(Number signed) {
     219        final long value = signed.longValue();
     220        final int shift = (value > Integer.MAX_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1;
     221        return convertLong((value << 1) ^ (value >>> shift));
     222    }
     223}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.IOException;
     5import java.nio.charset.StandardCharsets;
     6import java.util.stream.Stream;
     7
     8import org.openstreetmap.josm.tools.Utils;
     9
     10/**
     11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
     12 * @author Taylor Smock
     13 * @since xxx
     14 */
     15public class ProtoBufRecord implements AutoCloseable {
     16    private static final byte[] EMPTY_BYTES = {};
     17    private final WireType type;
     18    private final int field;
     19    private byte[] bytes;
     20
     21    /**
     22     * Create a new Protobuf record
     23     * @param parser The parser to use to create the record
     24     * @throws IOException - if an IO error occurs
     25     */
     26    public ProtoBufRecord(ProtoBufParser parser) throws IOException {
     27        Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
     28        // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
     29        this.field = (int) number.longValue() >> 3;
     30        // 7 is 111 (so last three bits)
     31        byte wireType = (byte) (number.longValue() & 7);
     32        this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst().orElse(WireType.UNKNOWN);
     33
     34        if (this.type == WireType.VARINT) {
     35            this.bytes = parser.nextVarInt();
     36        } else if (this.type == WireType.SIXTY_FOUR_BIT) {
     37            this.bytes = parser.nextFixed64();
     38        } else if (this.type == WireType.THIRTY_TWO_BIT) {
     39            this.bytes = parser.nextFixed32();
     40        } else if (this.type == WireType.LENGTH_DELIMITED) {
     41            this.bytes = parser.nextLengthDelimited();
     42        } else {
     43            this.bytes = EMPTY_BYTES;
     44        }
     45    }
     46
     47    /**
     48     * Get the field value
     49     * @return The field value
     50     */
     51    public int getField() {
     52        return this.field;
     53    }
     54
     55    /**
     56     * Get the WireType of the data
     57     * @return The {@link WireType} of the data
     58     */
     59    public WireType getType() {
     60        return this.type;
     61    }
     62
     63    /**
     64     * Get the raw bytes for this record
     65     * @return The bytes
     66     */
     67    public byte[] getBytes() {
     68        return this.bytes;
     69    }
     70
     71    /**
     72     * Get the var int ({@code WireType#VARINT})
     73     * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     74     */
     75    public Number asUnsignedVarInt() {
     76        return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
     77    }
     78
     79    /**
     80     * Get the signed var int ({@code WireType#VARINT}).
     81     * These are specially encoded so that they take up less space.
     82     *
     83     * @return The signed var int ({@code sint32} or {@code sint64})
     84     */
     85    public Number asSignedVarInt() {
     86        final Number signed = this.asUnsignedVarInt();
     87        return ProtoBufParser.decodeZigZag(signed);
     88    }
     89
     90    /**
     91     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     92     * @return the double
     93     */
     94    public double asDouble() {
     95        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     96        return Double.longBitsToDouble(doubleNumber);
     97    }
     98
     99    /**
     100     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     101     * @return the float
     102     */
     103    public float asFloat() {
     104        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     105        return Float.intBitsToFloat(floatNumber);
     106    }
     107
     108    /**
     109     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     110     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     111     */
     112    public String asString() {
     113        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     114    }
     115
     116    /**
     117     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     118     * @return a byte array of the 32 bits (4 bytes)
     119     */
     120    public byte[] asFixed32() {
     121        // TODO verify, or just assume?
     122        // 4 bytes == 32 bits
     123        return this.bytes;
     124    }
     125
     126    /**
     127     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     128     * @return a byte array of the 64 bits (8 bytes)
     129     */
     130    public byte[] asFixed64() {
     131        // TODO verify, or just assume?
     132        // 8 bytes == 64 bits
     133        return this.bytes;
     134    }
     135
     136    @Override
     137    public void close() {
     138        this.bytes = null;
     139    }
     140}
  • new file src/org/openstreetmap/josm/data/protobuf/WireType.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4/**
     5 * The WireTypes
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum WireType {
     10    /** int32, int64, uint32, uint64, sing32, sint64, bool, enum */
     11    VARINT(0),
     12    /** fixed64, sfixed64, double */
     13    SIXTY_FOUR_BIT(1),
     14    /** string, bytes, embedded messages, packed repeated fields */
     15    LENGTH_DELIMITED(2),
     16    /**
     17     * start groups
     18     * @deprecated Unknown reason. Deprecated since at least 2012.
     19     */
     20    @Deprecated
     21    START_GROUP(3),
     22    /**
     23     * end groups
     24     * @deprecated Unknown reason. Deprecated since at least 2012.
     25     */
     26    @Deprecated
     27    END_GROUP(4),
     28    /** fixed32, sfixed32, float */
     29    THIRTY_TWO_BIT(5),
     30
     31    /** For unknown WireTypes */
     32    UNKNOWN(Byte.MAX_VALUE);
     33
     34    private final byte type;
     35    WireType(int value) {
     36        this.type = (byte) value;
     37    }
     38
     39    /**
     40     * Get the type representation (byte form)
     41     * @return The wire type byte representation
     42     */
     43    public byte getTypeRepresentation() {
     44        return this.type;
     45    }
     46}
  • new file src/org/openstreetmap/josm/data/vector/VectorDataSet.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.awt.Color;
     6import java.awt.geom.Area;
     7import java.awt.geom.Ellipse2D;
     8import java.awt.geom.Path2D;
     9import java.awt.geom.PathIterator;
     10import java.awt.geom.Point2D;
     11import java.util.ArrayList;
     12import java.util.Arrays;
     13import java.util.Collection;
     14import java.util.Collections;
     15import java.util.HashMap;
     16import java.util.HashSet;
     17import java.util.LinkedList;
     18import java.util.List;
     19import java.util.Map;
     20import java.util.Optional;
     21import java.util.Set;
     22import java.util.concurrent.CopyOnWriteArrayList;
     23import java.util.concurrent.locks.Lock;
     24import java.util.concurrent.locks.ReentrantReadWriteLock;
     25import java.util.function.Predicate;
     26import java.util.stream.Collectors;
     27import java.util.stream.Stream;
     28
     29import org.openstreetmap.gui.jmapviewer.Coordinate;
     30import org.openstreetmap.gui.jmapviewer.Tile;
     31import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     32import org.openstreetmap.josm.data.DataSource;
     33import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     34import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     35import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     36import org.openstreetmap.josm.data.osm.BBox;
     37import org.openstreetmap.josm.data.osm.DataSelectionListener;
     38import org.openstreetmap.josm.data.osm.DownloadPolicy;
     39import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     40import org.openstreetmap.josm.data.osm.INode;
     41import org.openstreetmap.josm.data.osm.IPrimitive;
     42import org.openstreetmap.josm.data.osm.IRelation;
     43import org.openstreetmap.josm.data.osm.IWay;
     44import org.openstreetmap.josm.data.osm.OsmData;
     45import org.openstreetmap.josm.data.osm.PrimitiveId;
     46import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore;
     47import org.openstreetmap.josm.data.osm.Storage;
     48import org.openstreetmap.josm.data.osm.UploadPolicy;
     49import org.openstreetmap.josm.data.osm.WaySegment;
     50import org.openstreetmap.josm.data.osm.event.DataSetListener;
     51import org.openstreetmap.josm.tools.Geometry;
     52import org.openstreetmap.josm.tools.ListenerList;
     53import org.openstreetmap.josm.tools.Logging;
     54import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     55
     56import sun.reflect.generics.reflectiveObjects.NotImplementedException;
     57
     58/**
     59 * A data class for Vector Data
     60 * @author Taylor Smock
     61 * @since xxx
     62 */
     63public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
     64    private final Map<Integer, VectorDataStore> dataStoreMap = new HashMap<>();
     65    private final CopyOnWriteArrayList<DataSetListener> listeners = new CopyOnWriteArrayList<>();
     66    private final Collection<PrimitiveId> selected = new HashSet<>();
     67    // Both of these listener lists are useless, since they expect OsmPrimitives at this time
     68    private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
     69    private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
     70    private boolean lock = true;
     71    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
     72    private String name;
     73    /** The current zoom we are getting/adding to */
     74    private int zoom;
     75    /** Default to normal download policy */
     76    private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL;
     77    /** Default to a blocked upload policy */
     78    private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED;
     79
     80    @Override public Collection<DataSource> getDataSources() {
     81        final int currentZoom = this.zoom;
     82        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     83        return dataStore.getDataSources();
     84    }
     85
     86    /**
     87     * Add a data source
     88     * @param currentZoom the zoom
     89     * @param dataSource The datasource to add at the zoom level
     90     */
     91    public void addDataSource(int currentZoom, DataSource dataSource) {
     92        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     93        dataStore.addDataSource(dataSource);
     94    }
     95
     96    @Override public void lock() {
     97        this.lock = true;
     98    }
     99
     100    @Override public void unlock() {
     101        this.lock = false;
     102    }
     103
     104    @Override public boolean isLocked() {
     105        return this.lock;
     106    }
     107
     108    @Override public String getVersion() {
     109        return "8"; // TODO
     110    }
     111
     112    @Override public String getName() {
     113        return this.name;
     114    }
     115
     116    @Override public void setName(String name) {
     117        this.name = name;
     118    }
     119
     120    @Override public void addPrimitive(VectorPrimitive primitive) {
     121        final int currentZoom = this.zoom;
     122        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     123        if (!dataStore.getPrimitivesMap().containsKey(primitive.getPrimitiveId())) {
     124            dataStore.getPrimitivesMap().put(primitive.getPrimitiveId(), primitive);
     125            dataStore.getAllPrimitives().add(primitive);
     126        }
     127    }
     128
     129    @Override public void clear() {
     130        this.dataStoreMap.clear();
     131    }
     132
     133    @Override public List<VectorNode> searchNodes(BBox bbox) {
     134        final int currentZoom = this.zoom;
     135        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     136        return dataStore.getStore().searchNodes(bbox);
     137    }
     138
     139    @Override public boolean containsNode(VectorNode vectorNode) {
     140        final int currentZoom = this.zoom;
     141        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     142        return dataStore.getStore().containsNode(vectorNode);
     143    }
     144
     145    @Override public List<VectorWay> searchWays(BBox bbox) {
     146        final int currentZoom = this.zoom;
     147        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     148        return dataStore.getStore().searchWays(bbox);
     149    }
     150
     151    @Override public boolean containsWay(VectorWay vectorWay) {
     152        final int currentZoom = this.zoom;
     153        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     154        return dataStore.getStore().containsWay(vectorWay);
     155    }
     156
     157    @Override public List<VectorRelation> searchRelations(BBox bbox) {
     158        final int currentZoom = this.zoom;
     159        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     160        return dataStore.getStore().searchRelations(bbox);
     161    }
     162
     163    @Override public boolean containsRelation(VectorRelation vectorRelation) {
     164        final int currentZoom = this.zoom;
     165        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     166        return dataStore.getStore().containsRelation(vectorRelation);
     167    }
     168
     169    @Override public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
     170        final int currentZoom = this.zoom;
     171        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     172        return dataStore.getPrimitivesMap().get(primitiveId);
     173    }
     174
     175    @Override public <T extends VectorPrimitive> Collection<T> getPrimitives(
     176      Predicate<? super VectorPrimitive> predicate) {
     177        final int currentZoom = this.zoom;
     178        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     179        return new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate);
     180    }
     181
     182    @Override public Collection<VectorNode> getNodes() {
     183        return this.getPrimitives(VectorNode.class::isInstance);
     184    }
     185
     186    @Override public Collection<VectorWay> getWays() {
     187        return this.getPrimitives(VectorWay.class::isInstance);
     188    }
     189
     190    @Override public Collection<VectorRelation> getRelations() {
     191        return this.getPrimitives(VectorRelation.class::isInstance);
     192    }
     193
     194    @Override public DownloadPolicy getDownloadPolicy() {
     195        return this.downloadPolicy;
     196    }
     197
     198    @Override public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
     199        this.downloadPolicy = downloadPolicy;
     200    }
     201
     202    @Override public UploadPolicy getUploadPolicy() {
     203        return this.uploadPolicy;
     204    }
     205
     206    @Override public void setUploadPolicy(UploadPolicy uploadPolicy) {
     207        this.uploadPolicy = uploadPolicy;
     208    }
     209
     210    @Override public Lock getReadLock() {
     211        return this.readWriteLock.readLock();
     212    }
     213
     214    @Override public Collection<WaySegment> getHighlightedVirtualNodes() {
     215        // TODO?
     216        return Collections.emptyList();
     217    }
     218
     219    @Override public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
     220        // TODO?
     221    }
     222
     223    @Override public Collection<WaySegment> getHighlightedWaySegments() {
     224        // TODO?
     225        return Collections.emptyList();
     226    }
     227
     228    @Override public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
     229        // TODO?
     230    }
     231
     232    @Override public void addHighlightUpdateListener(HighlightUpdateListener listener) {
     233        this.highlightUpdateListenerListenerList.addListener(listener);
     234    }
     235
     236    @Override public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
     237        this.highlightUpdateListenerListenerList.removeListener(listener);
     238    }
     239
     240    @Override public Collection<VectorPrimitive> getAllSelected() {
     241        final int currentZoom = this.zoom;
     242        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     243        return dataStore.getAllPrimitives().stream().filter(primitive -> this.selected.contains(primitive.getPrimitiveId())).collect(
     244          Collectors.toList());
     245    }
     246
     247    @Override public boolean selectionEmpty() {
     248        return this.selected.isEmpty();
     249    }
     250
     251    @Override public boolean isSelected(VectorPrimitive osm) {
     252        return this.selected.contains(osm.getPrimitiveId());
     253    }
     254
     255    @Override public void toggleSelected(Collection<? extends PrimitiveId> osm) {
     256        this.toggleSelectedImpl(osm.stream());
     257    }
     258
     259    @Override public void toggleSelected(PrimitiveId... osm) {
     260        this.toggleSelectedImpl(Stream.of(osm));
     261    }
     262
     263    private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
     264        osm.forEach(primitiveId -> {
     265            if (this.selected.contains(primitiveId)) {
     266                this.selected.remove(primitiveId);
     267            } else {
     268                this.selected.add(primitiveId);
     269            }
     270        });
     271    }
     272
     273    @Override public void setSelected(Collection<? extends PrimitiveId> selection) {
     274        this.setSelectedImpl(selection.stream());
     275    }
     276
     277    @Override public void setSelected(PrimitiveId... osm) {
     278        this.setSelectedImpl(Stream.of(osm));
     279    }
     280
     281    private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
     282        this.selected.clear();
     283        osm.forEach(this.selected::add);
     284    }
     285
     286    @Override public void addSelected(Collection<? extends PrimitiveId> selection) {
     287        this.addSelectedImpl(selection.stream());
     288    }
     289
     290    @Override public void addSelected(PrimitiveId... osm) {
     291        this.addSelectedImpl(Stream.of(osm));
     292    }
     293
     294    private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
     295        osm.forEach(this.selected::add);
     296    }
     297
     298    @Override public void clearSelection(PrimitiveId... osm) {
     299        this.clearSelectionImpl(Stream.of(osm));
     300    }
     301
     302    @Override public void clearSelection(Collection<? extends PrimitiveId> list) {
     303        this.clearSelectionImpl(list.stream());
     304    }
     305
     306    @Override public void clearSelection() {
     307        this.clearSelectionImpl(new ArrayList<>(this.selected).stream());
     308    }
     309
     310    private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
     311        osm.forEach(this.selected::remove);
     312    }
     313
     314    @Override public void addSelectionListener(DataSelectionListener listener) {
     315        this.dataSelectionListenerListenerList.addListener(listener);
     316    }
     317
     318    @Override public void removeSelectionListener(DataSelectionListener listener) {
     319        this.dataSelectionListenerListenerList.removeListener(listener);
     320    }
     321
     322    @Override public void clearMappaintCache() {
     323        // TODO?
     324    }
     325
     326    public void setZoom(int zoom) {
     327        try {
     328            this.readWriteLock.writeLock().lockInterruptibly();
     329            this.zoom = zoom;
     330        } catch (InterruptedException e) {
     331            Logging.error(e);
     332            Thread.currentThread().interrupt();
     333        } finally {
     334            if (this.readWriteLock.isWriteLockedByCurrentThread()) {
     335                this.readWriteLock.writeLock().unlock();
     336            }
     337        }
     338    }
     339
     340    public int getZoom() {
     341        return this.zoom;
     342    }
     343
     344    public <T extends Tile & VectorTile> void addTileData(T tile) {
     345        ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
     346        ICoordinate lowerRight = tile.getTileSource().xyToLatLon(tile.getExtent(), tile.getExtent(), tile.getZoom());
     347        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(tile.getZoom(), VectorDataStore::new);
     348        try {
     349            this.readWriteLock.writeLock().lockInterruptibly();
     350            dataStore.addTile(tile);
     351        } catch (InterruptedException e) {
     352            Logging.error(e);
     353            Thread.currentThread().interrupt();
     354        } finally {
     355            if (this.readWriteLock.isWriteLockedByCurrentThread()) {
     356                this.readWriteLock.writeLock().unlock();
     357            }
     358        }
     359    }
     360
     361    private static class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> {
     362        protected final int zoom;
     363        protected final QuadBucketPrimitiveStore<N, W, R> store = new QuadBucketPrimitiveStore<>();
     364        protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
     365        protected final Set<Tile> addedTiles = new HashSet<>();
     366        protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
     367          .foreignKey(new Storage.PrimitiveIdHash());
     368        protected final Collection<DataSource> dataSources = new LinkedList<>();
     369
     370        public DataStore(int zoom) {
     371            this.zoom = zoom;
     372        }
     373
     374        public int getZoom() {
     375            return this.zoom;
     376        }
     377
     378        public QuadBucketPrimitiveStore<N, W, R> getStore() {
     379            return this.store;
     380        }
     381
     382        public Storage<O> getAllPrimitives() {
     383            return this.allPrimitives;
     384        }
     385
     386        public Map<PrimitiveId, O> getPrimitivesMap() {
     387            return this.primitivesMap;
     388        }
     389
     390        public Collection<DataSource> getDataSources() {
     391            return Collections.unmodifiableCollection(dataSources);
     392        }
     393
     394        public void addDataSource(DataSource dataSource) {
     395            this.dataSources.add(dataSource);
     396        }
     397
     398        protected void addPrimitive(O primitive) {
     399            this.store.addPrimitive(primitive);
     400            this.allPrimitives.add(primitive);
     401            this.primitivesMap.put(primitive.getPrimitiveId(), primitive);
     402        }
     403    }
     404
     405    private static class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
     406        public VectorDataStore(int zoom) {
     407            super(zoom);
     408        }
     409
     410        private <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, int x, int y) {
     411            final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
     412            final int layerExtent = layer.getExtent() * 2;
     413            final ICoordinate lowerRight = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     414            final ICoordinate coords = new Coordinate(upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent,
     415              upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent);
     416            final Collection<VectorNode> nodes = this.store.searchNodes(new BBox(coords.getLon(), coords.getLat()));
     417            if (!nodes.isEmpty()) {
     418                return nodes.iterator().next();
     419            }
     420            final VectorNode node = new VectorNode(layer.getName());
     421            node.setCoor(coords);
     422            featureObjects.add(node);
     423            return node;
     424        }
     425
     426        private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, Path2D shape) {
     427            final PathIterator pathIterator = shape.getPathIterator(null);
     428            return pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream().filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(
     429              Collectors.toList());
     430        }
     431
     432        private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) {
     433            final List<VectorNode> nodes = new ArrayList<>();
     434            final double[] coords = new double[6];
     435            final List<VectorPrimitive> ways = new ArrayList<>();
     436            do {
     437                final int type = pathIterator.currentSegment(coords);
     438                pathIterator.next();
     439                if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) {
     440                    if (PathIterator.SEG_CLOSE == type) {
     441                        nodes.add(nodes.get(0));
     442                    }
     443                    // New line
     444                    if (!nodes.isEmpty()) {
     445                        final VectorWay way = new VectorWay(layer.getName());
     446                        way.setNodes(nodes);
     447                        featureObjects.add(way);
     448                        ways.add(way);
     449                    }
     450                    nodes.clear();
     451                }
     452                if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) {
     453                    final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]);
     454                    nodes.add(node);
     455                } else if (PathIterator.SEG_CLOSE != type) {
     456                    // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time.
     457                    throw new NotImplementedException();
     458                }
     459            } while (!pathIterator.isDone());
     460            if (!nodes.isEmpty()) {
     461                final VectorWay way = new VectorWay(layer.getName());
     462                way.setNodes(nodes);
     463                featureObjects.add(way);
     464                ways.add(way);
     465            }
     466            return ways;
     467        }
     468
     469        private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, Area area) {
     470            final PathIterator pathIterator = area.getPathIterator(null);
     471            final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator);
     472            VectorRelation vectorRelation = new VectorRelation(layer.getName());
     473            for (VectorPrimitive member : members) {
     474                final String role;
     475                if (member instanceof VectorWay && ((VectorWay) member).isClosed()) {
     476                    role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner";
     477                } else {
     478                    role = "";
     479                }
     480                vectorRelation.addRelationMember(new VectorRelationMember(role, member));
     481            }
     482            return vectorRelation;
     483        }
     484
     485        public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
     486            Optional<Tile> previous = this.addedTiles.stream().filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
     487            // Check if we have already added the tile (just to save processing time)
     488            if (!previous.isPresent() || !previous.get().isLoaded() && !previous.get().isLoading()) {
     489                if (previous.isPresent()) {
     490                    this.addedTiles.remove(previous.get());
     491                }
     492                this.addedTiles.add(tile);
     493                for (Layer layer : tile.getLayers()) {
     494                    layer.getGeometry().forEach(geometry -> {
     495                        Collection<VectorPrimitive> primitives = new ArrayList<>(geometry.getShapes().size());
     496                        List<VectorPrimitive> featureObjects = new ArrayList<>();
     497                        List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
     498                        geometry.getShapes().forEach(shape -> {
     499                            final VectorPrimitive primitive;
     500                            if (shape instanceof Ellipse2D) {
     501                                primitive = pointToNode(tile, layer, featureObjects, (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
     502                            } else if (shape instanceof Path2D) {
     503                                primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst().orElse(null);
     504                            } else if (shape instanceof Area) {
     505                                primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
     506                            } else {
     507                                // We shouldn't hit this, but just in case
     508                                throw new NotImplementedException();
     509                            }
     510                            primaryFeatureObjects.add(primitive);
     511                        });
     512                        final VectorPrimitive primitive;
     513                        if (primaryFeatureObjects.size() == 1) {
     514                            primitive = primaryFeatureObjects.get(0);
     515                            if (primitive instanceof IRelation) {
     516                                // This should always be a multipolygon
     517                                primitive.put("type", "multipolygon");
     518                            }
     519                        } else if (!primaryFeatureObjects.isEmpty()) {
     520                            VectorRelation relation = new VectorRelation(layer.getName());
     521                            primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)).forEach(relation::addRelationMember);
     522                            primitive = relation;
     523                        } else {
     524                            return;
     525                        }
     526                        Feature feature = geometry.getFeature();
     527                        primitive.setId(feature.getId());
     528                        feature.getTags().forEach(primitive::put);
     529                        featureObjects.forEach(this::addPrimitive);
     530                        primaryFeatureObjects.forEach(this::addPrimitive);
     531                        this.addPrimitive(primitive);
     532                    });
     533                }
     534            }
     535        }
     536    }
     537}
  • new file src/org/openstreetmap/josm/data/vector/VectorNode.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import java.util.List;
     4
     5import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     6import org.openstreetmap.josm.data.coor.EastNorth;
     7import org.openstreetmap.josm.data.coor.LatLon;
     8import org.openstreetmap.josm.data.osm.BBox;
     9import org.openstreetmap.josm.data.osm.INode;
     10import org.openstreetmap.josm.data.osm.IPrimitive;
     11import org.openstreetmap.josm.data.osm.IWay;
     12import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     13import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     14import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     15import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     16
     17/**
     18 * The "Node" type of a vector layer
     19 * @since xxx
     20 */
     21public class VectorNode extends VectorPrimitive implements INode {
     22    private static final UniqueIdGenerator ID_GENERATOR = new UniqueIdGenerator();
     23    private double lon = Double.NaN;
     24    private double lat = Double.NaN;
     25
     26    public VectorNode(String layer) {
     27        super(layer);
     28    }
     29
     30    @Override public double lon() {
     31        return this.lon;
     32    }
     33
     34    @Override public double lat() {
     35        return this.lat;
     36    }
     37
     38    @Override public UniqueIdGenerator getIdGenerator() {
     39        return ID_GENERATOR;
     40    }
     41
     42    @Override public LatLon getCoor() {
     43        return new LatLon(this.lat, this.lon);
     44    }
     45
     46    @Override public void setCoor(LatLon coor) {
     47        this.lat = coor.lat();
     48        this.lon = coor.lon();
     49    }
     50
     51    /**
     52     * Set the coordinates of this node
     53     * @param coordinates The coordinates to set
     54     * @see #setCoor(LatLon)
     55     */
     56    public void setCoor(ICoordinate coordinates) {
     57        this.lat = coordinates.getLat();
     58        this.lon = coordinates.getLon();
     59    }
     60
     61    @Override public void setEastNorth(EastNorth eastNorth) {
     62        final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth);
     63        this.lat = ll.lat();
     64        this.lon = ll.lon();
     65    }
     66
     67    @Override public boolean isReferredByWays(int n) {
     68        // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
     69        // when way is cloned
     70        List<? extends IPrimitive> referrers = super.getReferrers();
     71        if (referrers == null || referrers.isEmpty()) return false;
     72        if (referrers instanceof IPrimitive)
     73            return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet();
     74        else {
     75            int counter = 0;
     76            for (IPrimitive o : referrers) {
     77                if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n)
     78                    return true;
     79            }
     80            return false;
     81        }
     82    }
     83
     84    @Override public void accept(PrimitiveVisitor visitor) {
     85        visitor.visit(this);
     86    }
     87
     88    @Override public BBox getBBox() {
     89        return new BBox(this.lon, this.lat);
     90    }
     91
     92    @Override public OsmPrimitiveType getType() {
     93        return OsmPrimitiveType.NODE;
     94    }
     95}
  • new file src/org/openstreetmap/josm/data/vector/VectorPrimitive.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import java.util.Arrays;
     4import java.util.List;
     5import java.util.Map;
     6import java.util.function.Consumer;
     7import java.util.stream.Collectors;
     8import java.util.stream.IntStream;
     9import java.util.stream.Stream;
     10
     11import org.openstreetmap.josm.data.osm.AbstractPrimitive;
     12import org.openstreetmap.josm.data.osm.IPrimitive;
     13import org.openstreetmap.josm.data.osm.NameFormatter;
     14import org.openstreetmap.josm.data.osm.OsmPrimitive;
     15import org.openstreetmap.josm.data.osm.TagMap;
     16import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     17import org.openstreetmap.josm.gui.mappaint.StyleCache;
     18import org.openstreetmap.josm.tools.Utils;
     19
     20/**
     21 * The base class for Vector primitives
     22 * @author Taylor Smock
     23 * @since xxx
     24 */
     25public abstract class VectorPrimitive extends AbstractPrimitive {
     26    private VectorDataSet dataSet;
     27    private TagMap tags;
     28    private boolean highlighted;
     29    private StyleCache mappaintStyle;
     30    private boolean mappaintStyleUpToDate;
     31    private final String layer;
     32
     33    public VectorPrimitive(String layer) {
     34        this.layer = layer;
     35    }
     36
     37    @Override protected void keysChangedImpl(Map<String, String> originalKeys) {
     38        clearCachedStyle();
     39        if (dataSet != null) {
     40            for (IPrimitive ref : getReferrers()) {
     41                ref.clearCachedStyle();
     42            }
     43        }
     44    }
     45
     46    @Override public boolean isHighlighted() {
     47        return this.highlighted;
     48    }
     49
     50    @Override public void setHighlighted(boolean highlighted) {
     51        this.highlighted = highlighted;
     52    }
     53
     54    @Override public boolean isTagged() {
     55        return !this.getInterestingTags().isEmpty();
     56    }
     57
     58    @Override public boolean isAnnotated() {
     59        return this.getInterestingTags().size() - this.tags.size() > 0;
     60    }
     61
     62    @Override
     63    public VectorDataSet getDataSet() {
     64        return this.dataSet;
     65    }
     66
     67    protected void setDataSet(VectorDataSet dataSet) {
     68        this.dataSet = dataSet;
     69    }
     70
     71    @Override public StyleCache getCachedStyle() {
     72        return this.mappaintStyle;
     73    }
     74
     75    @Override public void setCachedStyle(StyleCache mappaintStyle) {
     76        this.mappaintStyle = mappaintStyle;
     77        if (mappaintStyle != null) {
     78            this.declareCachedStyleUpToDate();
     79        } else {
     80            this.mappaintStyleUpToDate = false;
     81        }
     82    }
     83
     84    @Override public boolean isCachedStyleUpToDate() {
     85        return this.mappaintStyleUpToDate;
     86    }
     87
     88    @Override public void declareCachedStyleUpToDate() {
     89        this.mappaintStyleUpToDate = true;
     90        this.clearCachedStyle();
     91    }
     92
     93    @Override public boolean hasDirectionKeys() {
     94        return false;
     95    }
     96
     97    @Override public boolean reversedDirection() {
     98        return false;
     99    }
     100
     101    /*------------
     102     * Referrers
     103     ------------*/
     104    // Largely the same as OsmPrimitive, OsmPrimitive not modified at this time to avoid breaking binary compatibility
     105
     106    private Object referrers;
     107
     108    @Override
     109    public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) {
     110        return referrers(allowWithoutDataset, OsmPrimitive.class)
     111          .collect(Collectors.toList());
     112    }
     113
     114    /**
     115     * Add new referrer. If referrer is already included then no action is taken
     116     * @param referrer The referrer to add
     117     */
     118    protected void addReferrer(IPrimitive referrer) {
     119        if (referrers == null) {
     120            referrers = referrer;
     121        } else if (referrers instanceof IPrimitive) {
     122            if (referrers != referrer) {
     123                referrers = new IPrimitive[] {(IPrimitive) referrers, referrer};
     124            }
     125        } else {
     126            for (IPrimitive primitive:(IPrimitive[]) referrers) {
     127                if (primitive == referrer)
     128                    return;
     129            }
     130            referrers = Utils.addInArrayCopy((IPrimitive[]) referrers, referrer);
     131        }
     132    }
     133
     134    /**
     135     * Remove referrer. No action is taken if referrer is not registered
     136     * @param referrer The referrer to remove
     137     */
     138    protected void removeReferrer(IPrimitive referrer) {
     139        if (referrers instanceof IPrimitive) {
     140            if (referrers == referrer) {
     141                referrers = null;
     142            }
     143        } else if (referrers instanceof IPrimitive[]) {
     144            IPrimitive[] orig = (IPrimitive[]) referrers;
     145            int idx = IntStream.range(0, orig.length)
     146              .filter(i -> orig[i] == referrer)
     147              .findFirst().orElse(-1);
     148            if (idx == -1)
     149                return;
     150
     151            if (orig.length == 2) {
     152                referrers = orig[1-idx]; // idx is either 0 or 1, take the other
     153            } else { // downsize the array
     154                IPrimitive[] smaller = new IPrimitive[orig.length-1];
     155                System.arraycopy(orig, 0, smaller, 0, idx);
     156                System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx);
     157                referrers = smaller;
     158            }
     159        }
     160    }
     161
     162    private <T extends IPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) {
     163        // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example
     164        // when way is cloned
     165
     166        if (dataSet == null && allowWithoutDataset) {
     167            return Stream.empty();
     168        }
     169        if (referrers == null) {
     170            return Stream.empty();
     171        }
     172        final Stream<IPrimitive> stream = referrers instanceof IPrimitive // NOPMD
     173          ? Stream.of((IPrimitive) referrers)
     174          : Arrays.stream((IPrimitive[]) referrers);
     175        return stream
     176          .filter(p -> p.getDataSet() == dataSet)
     177          .filter(filter::isInstance)
     178          .map(filter::cast);
     179    }
     180
     181    /**
     182     * Gets all primitives in the current dataset that reference this primitive.
     183     * @param filter restrict primitives to subclasses
     184     * @param <T> type of primitives
     185     * @return the referrers as Stream
     186     */
     187    public final <T extends IPrimitive> Stream<T> referrers(Class<T> filter) {
     188        return referrers(false, filter);
     189    }
     190
     191    @Override
     192    public void visitReferrers(PrimitiveVisitor visitor) {
     193        if (visitor != null)
     194            doVisitReferrers(o -> o.accept(visitor));
     195    }
     196
     197    private void doVisitReferrers(Consumer<IPrimitive> visitor) {
     198        if (this.referrers == null)
     199            return;
     200        else if (this.referrers instanceof IPrimitive) {
     201            IPrimitive ref = (IPrimitive) this.referrers;
     202            if (ref.getDataSet() == dataSet) {
     203                visitor.accept(ref);
     204            }
     205        } else if (this.referrers instanceof IPrimitive[]) {
     206            IPrimitive[] refs = (IPrimitive[]) this.referrers;
     207            for (IPrimitive ref: refs) {
     208                if (ref.getDataSet() == dataSet) {
     209                    visitor.accept(ref);
     210                }
     211            }
     212        }
     213    }
     214
     215    /**
     216     * Set the id of the object
     217     * @param id The id
     218     */
     219    protected void setId(long id) {
     220        this.id = id;
     221    }
     222}
  • new file src/org/openstreetmap/josm/data/vector/VectorRelation.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import java.util.ArrayList;
     4import java.util.Collections;
     5import java.util.List;
     6
     7import org.openstreetmap.josm.data.osm.BBox;
     8import org.openstreetmap.josm.data.osm.IPrimitive;
     9import org.openstreetmap.josm.data.osm.IRelation;
     10import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     11import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     12import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     13
     14/**
     15 * The "Relation" type for vectors
     16 * @author Taylor Smock
     17 * @since xxx
     18 */
     19public class VectorRelation extends VectorPrimitive implements IRelation<VectorRelationMember> {
     20    private static final UniqueIdGenerator RELATION_ID_GENERATOR = new UniqueIdGenerator();
     21    private final List<VectorRelationMember> members = new ArrayList<>();
     22    public VectorRelation(String layer) {
     23        super(layer);
     24    }
     25    @Override public UniqueIdGenerator getIdGenerator() {
     26        return RELATION_ID_GENERATOR;
     27    }
     28
     29    @Override public void accept(PrimitiveVisitor visitor) {
     30        visitor.visit(this);
     31    }
     32
     33    @Override public BBox getBBox() {
     34        final BBox bbox = new BBox();
     35        for (IPrimitive member : this.getMemberPrimitivesList()) {
     36            bbox.add(member.getBBox());
     37        }
     38        return bbox;
     39    }
     40
     41    protected void addRelationMember(VectorRelationMember member) {
     42        this.members.add(member);
     43    }
     44
     45    @Override public int getMembersCount() {
     46        return this.members.size();
     47    }
     48
     49    @Override public VectorRelationMember getMember(int index) {
     50        return this.members.get(index);
     51    }
     52
     53    @Override public List<VectorRelationMember> getMembers() {
     54        return Collections.unmodifiableList(this.members);
     55    }
     56
     57    @Override public void setMembers(List<VectorRelationMember> members) {
     58        this.members.clear();
     59        this.members.addAll(members);
     60    }
     61
     62    @Override public long getMemberId(int idx) {
     63        return this.getMember(idx).getMember().getId();
     64    }
     65
     66    @Override public String getRole(int idx) {
     67        return this.getMember(idx).getRole();
     68    }
     69
     70    @Override public OsmPrimitiveType getMemberType(int idx) {
     71        return this.getMember(idx).getType();
     72    }
     73
     74    @Override public OsmPrimitiveType getType() {
     75        return this.getMembers().stream().map(VectorRelationMember::getType).allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
     76    }
     77}
  • new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import java.util.Optional;
     4
     5import org.openstreetmap.josm.data.osm.INode;
     6import org.openstreetmap.josm.data.osm.IRelation;
     7import org.openstreetmap.josm.data.osm.IRelationMember;
     8import org.openstreetmap.josm.data.osm.IWay;
     9import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     10import org.openstreetmap.josm.tools.CheckParameterUtil;
     11
     12public class VectorRelationMember implements IRelationMember<VectorPrimitive> {
     13    private final String role;
     14    private final VectorPrimitive member;
     15    public VectorRelationMember(String role, VectorPrimitive member) {
     16        CheckParameterUtil.ensureParameterNotNull(member, "member");
     17        this.role = Optional.ofNullable(role).orElse("").intern();
     18        this.member = member;
     19    }
     20    @Override public String getRole() {
     21        return this.role;
     22    }
     23
     24    @Override public boolean isNode() {
     25        return this.member instanceof INode;
     26    }
     27
     28    @Override public boolean isWay() {
     29        return this.member instanceof IWay;
     30    }
     31
     32    @Override public boolean isRelation() {
     33        return this.member instanceof IRelation;
     34    }
     35
     36    @Override public VectorPrimitive getMember() {
     37        return this.member;
     38    }
     39
     40    @Override public long getUniqueId() {
     41        return this.member.getId();
     42    }
     43
     44    @Override public OsmPrimitiveType getType() {
     45        return this.member.getType();
     46    }
     47
     48    @Override public boolean isNew() {
     49        return this.member.isNew();
     50    }
     51}
  • new file src/org/openstreetmap/josm/data/vector/VectorWay.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java
    new file mode 100644
    - +  
     1package org.openstreetmap.josm.data.vector;
     2
     3import java.util.ArrayList;
     4import java.util.Collections;
     5import java.util.List;
     6import java.util.stream.Collectors;
     7
     8import org.openstreetmap.josm.data.osm.BBox;
     9import org.openstreetmap.josm.data.osm.INode;
     10import org.openstreetmap.josm.data.osm.IWay;
     11import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     12import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     13import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     14
     15/**
     16 * The "Way" type for a Vector layer
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public class VectorWay extends VectorPrimitive implements IWay<VectorNode> {
     21    private static final UniqueIdGenerator WAY_GENERATOR = new UniqueIdGenerator();
     22    private final List<VectorNode> nodes = new ArrayList<>();
     23    public VectorWay(String layer) {
     24        super(layer);
     25    }
     26    @Override public UniqueIdGenerator getIdGenerator() {
     27        return WAY_GENERATOR;
     28    }
     29
     30    @Override public void accept(PrimitiveVisitor visitor) {
     31        visitor.visit(this);
     32    }
     33
     34    @Override public BBox getBBox() {
     35        final BBox bbox = new BBox();
     36        for (INode node : this.getNodes()) {
     37            bbox.add(node.getBBox());
     38        }
     39        return bbox;
     40    }
     41
     42    @Override public int getNodesCount() {
     43        return this.getNodes().size();
     44    }
     45
     46    @Override public VectorNode getNode(int index) {
     47        return this.getNodes().get(index);
     48    }
     49
     50    @Override public List<VectorNode> getNodes() {
     51        return Collections.unmodifiableList(this.nodes);
     52    }
     53
     54    @Override public List<Long> getNodeIds() {
     55        return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList());
     56    }
     57
     58    @Override public long getNodeId(int idx) {
     59        return this.getNodes().get(idx).getId();
     60    }
     61
     62    @Override public void setNodes(List<VectorNode> nodes) {
     63        this.nodes.clear();
     64        this.nodes.addAll(nodes);
     65    }
     66
     67    @Override public boolean isClosed() {
     68        return this.firstNode() != null && this.firstNode().equals(this.lastNode());
     69    }
     70
     71    @Override public VectorNode firstNode() {
     72        if (this.nodes.isEmpty()) {
     73            return null;
     74        }
     75        return this.getNode(0);
     76    }
     77
     78    @Override public VectorNode lastNode() {
     79        if (this.nodes.isEmpty()) {
     80            return null;
     81        }
     82        return this.getNode(this.getNodesCount() - 1);
     83    }
     84
     85    @Override public boolean isFirstLastNode(INode n) {
     86        if (this.nodes.isEmpty()) {
     87            return false;
     88        }
     89        return this.firstNode().equals(n) || this.lastNode().equals(n);
     90    }
     91
     92    @Override public boolean isInnerNode(INode n) {
     93        if (this.nodes.isEmpty()) {
     94            return false;
     95        }
     96        return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream().anyMatch(vectorNode -> vectorNode.equals(n));
     97    }
     98
     99    @Override public OsmPrimitiveType getType() {
     100        return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
     101    }
     102}
  • src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java b/src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java
    a b  
    88import java.util.ArrayList;
    99import java.util.Arrays;
    1010import java.util.Collections;
     11import java.util.EnumSet;
    1112import java.util.HashSet;
    1213import java.util.List;
    1314import java.util.Set;
     15import java.util.regex.Matcher;
     16import java.util.regex.Pattern;
    1417import java.util.stream.Collectors;
    1518
    1619import javax.imageio.ImageIO;
     
    1922import org.openstreetmap.josm.gui.layer.GpxLayer;
    2023import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
    2124import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     25import org.openstreetmap.josm.io.CachedFile;
    2226import org.openstreetmap.josm.io.IllegalDataException;
    2327
    2428/**
     
    2630 * @since 17548
    2731 */
    2832public class ImageImporter extends FileImporter {
     33
     34    /** Check if the filename starts with a borked path ({@link java.io.File#File} drops consecutive {@code /} characters). */
     35    private static final Pattern URL_START_BAD = Pattern.compile("^(https?:/)([^/].*)$");
     36    /** Check for the beginning of a "good" url */
     37    private static final Pattern URL_START_GOOD = Pattern.compile("^https?://.*$");
     38
    2939    private GpxLayer gpx;
    3040
    3141    /**
     
    90100        try {
    91101            List<File> files = new ArrayList<>();
    92102            Set<String> visitedDirs = new HashSet<>();
    93             addRecursiveFiles(files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true));
     103            addRecursiveFiles(this.options, files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true));
    94104
    95105            if (progressMonitor.isCanceled())
    96106                return;
     
    106116
    107117    static void addRecursiveFiles(List<File> files, Set<String> visitedDirs, List<File> sel, ProgressMonitor progressMonitor)
    108118            throws IOException {
     119        addRecursiveFiles(EnumSet.noneOf(Options.class), files, visitedDirs, sel, progressMonitor);
     120    }
     121
     122    static void addRecursiveFiles(Set<Options> options, List<File> files, Set<String> visitedDirs, List<File> sel,
     123            ProgressMonitor progressMonitor) throws IOException {
    109124
    110125        if (progressMonitor.isCanceled())
    111126            return;
     
    117132                    if (visitedDirs.add(f.getCanonicalPath())) { // Do not loop over symlinks
    118133                        File[] dirFiles = f.listFiles(); // Can be null for some strange directories (like lost+found)
    119134                        if (dirFiles != null) {
    120                             addRecursiveFiles(files, visitedDirs, Arrays.asList(dirFiles), progressMonitor.createSubTaskMonitor(1, true));
     135                            addRecursiveFiles(options, files, visitedDirs, Arrays.asList(dirFiles),
     136                                    progressMonitor.createSubTaskMonitor(1, true));
    121137                        }
    122138                    } else {
    123139                        progressMonitor.worked(1);
    124140                    }
    125141                } else {
    126                     if (FILE_FILTER.accept(f)) {
     142                    /* Check if the path is a web path, and if so, ensure that it is "correct" */
     143                    final String path = f.getPath();
     144                    Matcher matcherBad = URL_START_BAD.matcher(path);
     145                    final String realPath;
     146                    if (matcherBad.matches()) {
     147                        realPath = matcherBad.replaceFirst(matcherBad.group(1) + "/" + matcherBad.group(2));
     148                    } else {
     149                        realPath = path;
     150                    }
     151                    if (URL_START_GOOD.matcher(realPath).matches() && FILE_FILTER.accept(f)
     152                            && options.contains(Options.ALLOW_WEB_RESOURCES)) {
     153                        try (CachedFile cachedFile = new CachedFile(realPath)) {
     154                            files.add(cachedFile.getFile());
     155                        }
     156                    } else if (FILE_FILTER.accept(f)) {
    127157                        files.add(f);
    128158                    }
    129159                    progressMonitor.worked(1);
  • new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.awt.Component;
     6import java.awt.Graphics2D;
     7import java.awt.event.ActionEvent;
     8import java.util.ArrayList;
     9import java.util.Arrays;
     10import java.util.Collection;
     11import java.util.Collections;
     12import java.util.HashMap;
     13import java.util.List;
     14import java.util.Map;
     15import java.util.function.BooleanSupplier;
     16import java.util.function.Consumer;
     17import java.util.stream.Collectors;
     18
     19import javax.swing.AbstractAction;
     20import javax.swing.Action;
     21import javax.swing.JCheckBoxMenuItem;
     22
     23import org.openstreetmap.gui.jmapviewer.Tile;
     24import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     25import org.openstreetmap.josm.data.Bounds;
     26import org.openstreetmap.josm.data.imagery.ImageryInfo;
     27import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     28import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
     29import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     30import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.LayerShower;
     31import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener;
     32import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     33import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     34import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     35import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     36import org.openstreetmap.josm.data.vector.VectorDataSet;
     37import org.openstreetmap.josm.gui.MainApplication;
     38import org.openstreetmap.josm.gui.MapView;
     39import org.openstreetmap.josm.gui.NavigatableComponent;
     40import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
     41import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     42import org.openstreetmap.josm.spi.preferences.Config;
     43
     44/**
     45 * A layer for MapBox Vector Tiles
     46 * @author Taylor Smock
     47 * @since xxx
     48 */
     49public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements LayerShower, TileListener,
     50  NavigatableComponent.ZoomChangeListener {
     51    private static final String CACHE_REGION_NAME = "MVT";
     52    private final Map<String, Boolean> layerNames = new HashMap<>();
     53    private final VectorDataSet dataSet = new VectorDataSet();
     54
     55    /**
     56     * Creates an instance of an MVT layer
     57     *
     58     * @param info ImageryInfo describing the layer
     59     */
     60    public MVTLayer(ImageryInfo info) {
     61        super(info);
     62        NavigatableComponent.addZoomChangeListener(this);
     63    }
     64
     65    @Override
     66    protected Class<? extends TileLoader> getTileLoaderClass() {
     67        return MapBoxVectorCachedTileLoader.class;
     68    }
     69
     70    @Override
     71    protected String getCacheName() {
     72        return CACHE_REGION_NAME;
     73    }
     74
     75    @Override
     76    public Collection<String> getNativeProjections() {
     77        // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
     78        // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
     79        return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
     80    }
     81
     82    @Override public void paint(Graphics2D g, MapView mv, Bounds box) {
     83        this.dataSet.setZoom(this.getZoomLevel());
     84        boolean active = mv.getLayerManager().getActiveLayer() == this;
     85        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
     86        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
     87        AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
     88        painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
     89          || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     90        painter.render(this.dataSet, virtual, box);
     91    }
     92
     93    @Override
     94    protected MapboxVectorTileSource getTileSource() {
     95        MapboxVectorTileSource source = new MapboxVectorTileSource(this.info);
     96        this.info.setAttribution(source);
     97        return source;
     98    }
     99
     100    @Override
     101    public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
     102        final MVTTile tile = new MVTTile(source, x, y, zoom);
     103        tile.setLayerShower(this);
     104        tile.addTileLoaderFinisher(this);
     105        return tile;
     106    }
     107
     108    @Override
     109    public Action[] getMenuEntries() {
     110        ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries()));
     111        // Add separator between Info and the layers
     112        actions.add(SeparatorLayerAction.INSTANCE);
     113        for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
     114            actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
     115                    layer -> {layerNames.compute(layer, (key, value) -> !value); this.invalidate(); }));
     116        }
     117        return actions.toArray(new Action[0]);
     118    }
     119
     120    private static class EnableLayerAction extends AbstractAction implements LayerAction {
     121        private final String layer;
     122        private final Consumer<String> consumer;
     123        private final BooleanSupplier state;
     124        public EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) {
     125            super(tr("Toggle layer {0}", layer));
     126            this.layer = layer;
     127            this.consumer = consumer;
     128            this.state = state;
     129        }
     130        @Override
     131        public void actionPerformed(ActionEvent e) {
     132            consumer.accept(layer);
     133        }
     134        @Override
     135        public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
     136            return layers.stream().allMatch(MVTLayer.class::isInstance);
     137        }
     138        @Override
     139        public Component createMenuComponent() {
     140            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     141            item.setSelected(this.state.getAsBoolean());
     142            return item;
     143        }
     144    }
     145
     146    @Override
     147    public void finishedLoading(MVTTile tile) {
     148        for (Layer layer : tile.getLayers()) {
     149            this.layerNames.putIfAbsent(layer.getName(), true);
     150        }
     151        this.dataSet.addTileData(tile);
     152    }
     153
     154    @Override
     155    public List<String> layersToShow() {
     156        return this.layerNames.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList());
     157    }
     158}
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    a b  
    8787import org.openstreetmap.josm.data.imagery.OffsetBookmark;
    8888import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    8989import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     90import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
    9091import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    9192import org.openstreetmap.josm.data.preferences.BooleanProperty;
    9293import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    110111import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction;
    111112import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction;
    112113import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction;
     114import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
    113115import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
    114116import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction;
    115117import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
     
    890892            if (coordinateConverter.requiresReprojection()) {
    891893                tile = new ReprojectionTile(tileSource, x, y, zoom);
    892894            } else {
    893                 tile = new Tile(tileSource, x, y, zoom);
     895                tile = createTile(tileSource, x, y, zoom);
    894896            }
    895897            tileCache.addTile(tile);
    896898        }
     
    10291031        }
    10301032    }
    10311033
     1034    /**
     1035     * Draw a vector tile on screen.
     1036     * @param g the Graphics2D
     1037     * @param tile the vector tile
     1038     * @param anchorImage tile anchor in image coordinates
     1039     * @param anchorScreen tile anchor in screen coordinates
     1040     * @param clip clipping region in screen coordinates (can be null)
     1041     */
     1042    private void drawVectorTileInside(Graphics2D g, VectorTile tile, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
     1043        AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
     1044        Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null);
     1045        Point2D screen1 = imageToScreen.transform(new Point2D.Double(
     1046                tile.getExtent(), tile.getExtent()), null);
     1047
     1048        Shape oldClip = null;
     1049        if (clip != null) {
     1050            oldClip = g.getClip();
     1051            g.clip(clip);
     1052        }
     1053        tile.paint(g, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
     1054                (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
     1055                (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this.currentZoomLevel, this);
     1056        if (clip != null) {
     1057            g.setClip(oldClip);
     1058        }
     1059    }
     1060
    10321061    private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
    10331062        Object paintMutex = new Object();
    10341063        List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
     
    10431072                    img = getLoadedTileImage(tile);
    10441073                    anchorImage = getAnchor(tile, img);
    10451074                }
    1046                 if (img == null || anchorImage == null) {
     1075                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10471076                    miss = true;
    10481077                }
    10491078            }
     
    10521081                return;
    10531082            }
    10541083
    1055             img = applyImageProcessors(img);
     1084            if (img != null) {
     1085                img = applyImageProcessors(img);
     1086            }
    10561087
    10571088            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10581089            synchronized (paintMutex) {
    10591090                //cannot paint in parallel
    1060                 drawImageInside(g, img, anchorImage, anchorScreen, null);
     1091                if (tile instanceof VectorTile) {
     1092                    // drawVectorTileInside(g, (VectorTile) tile, anchorImage, anchorScreen, null); TODO
     1093                } else {
     1094                    drawImageInside(g, img, anchorImage, anchorScreen, null);
     1095                }
    10611096            }
    10621097            MapView mapView = MainApplication.getMap().mapView;
    10631098            if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
     
    18641899
    18651900                for (int x = minX; x <= maxX; x++) {
    18661901                    for (int y = minY; y <= maxY; y++) {
    1867                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1902                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18681903                    }
    18691904                }
    18701905            }
     
    19702005        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19712006    }
    19722007
     2008    /**
     2009     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     2010     *
     2011     * @param source Tile source
     2012     * @param x X coordinate
     2013     * @param y Y coordinate
     2014     * @param zoom Zoom level
     2015     * @return The new {@link Tile}
     2016     * @since xxx
     2017     */
     2018    public Tile createTile(T source, int x, int y, int zoom) {
     2019        return new Tile(source, x, y, zoom);
     2020    }
     2021
    19732022    @Override
    19742023    public synchronized void destroy() {
    19752024        super.destroy();
     
    19902039            allocateCacheMemory();
    19912040            if (memory != null) {
    19922041                doPaint(graphics);
     2042                if (AbstractTileSourceLayer.this instanceof MVTLayer) {
     2043                    AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView()
     2044                      .getRealBounds());
     2045                }
    19932046            } else {
    19942047                Graphics g = graphics.getDefaultGraphics();
    19952048                Color oldColor = g.getColor();
  • src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
    a b  
    3737import org.openstreetmap.josm.gui.MapView;
    3838import org.openstreetmap.josm.gui.MenuScroller;
    3939import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
     40import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
    4041import org.openstreetmap.josm.gui.widgets.UrlLabel;
    4142import org.openstreetmap.josm.tools.GBC;
    4243import org.openstreetmap.josm.tools.ImageProcessor;
     
    168169        case BING:
    169170        case SCANEX:
    170171            return new TMSLayer(info);
     172        case MVT:
     173            return new MVTLayer(info);
    171174        default:
    172175            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
    173176        }
  • new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.preferences.imagery;
     3import org.openstreetmap.josm.data.imagery.ImageryInfo;
     4import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
     5import org.openstreetmap.josm.gui.widgets.JosmTextArea;
     6import org.openstreetmap.josm.gui.widgets.JosmTextField;
     7import org.openstreetmap.josm.tools.GBC;
     8import org.openstreetmap.josm.tools.Utils;
     9
     10import javax.swing.JLabel;
     11import java.awt.event.KeyAdapter;
     12import java.awt.event.KeyEvent;
     13import java.util.Arrays;
     14
     15import static org.openstreetmap.josm.tools.I18n.tr;
     16
     17/**
     18 * A panel for adding MapBox Vector Tile layers
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class AddMVTLayerPanel extends AddImageryPanel {
     23    private final JosmTextField mvtZoom = new JosmTextField();
     24    private final JosmTextArea mvtUrl = new JosmTextArea(3, 40).transferFocusOnTab();
     25
     26    /**
     27     * Constructs a new {@code AddMVTLayerPanel}.
     28     */
     29    public AddMVTLayerPanel() {
     30
     31        add(new JLabel(tr("{0} Make sure OSM has the permission to use this service", "1.")), GBC.eol());
     32        add(new JLabel(tr("{0} Enter URL", "2.")), GBC.eol());
     33        add(new JLabel("<html>" + Utils.joinAsHtmlUnorderedList(Arrays.asList(
     34                tr("{0} is replaced by tile zoom level, also supported:<br>" +
     35                        "offsets to the zoom level: {1} or {2}<br>" +
     36                        "reversed zoom level: {3}", "{zoom}", "{zoom+1}", "{zoom-1}", "{19-zoom}"),
     37                tr("{0} is replaced by X-coordinate of the tile", "{x}"),
     38                tr("{0} is replaced by Y-coordinate of the tile", "{y}"),
     39                tr("{0} is replaced by a random selection from the given comma separated list, e.g. {1}", "{switch:...}", "{switch:a,b,c}")
     40        )) + "</html>"), GBC.eol().fill());
     41
     42        final KeyAdapter keyAdapter = new KeyAdapter() {
     43            @Override
     44            public void keyReleased(KeyEvent e) {
     45                mvtUrl.setText(buildMvtUrl());
     46            }
     47        };
     48
     49        add(rawUrl, GBC.eop().fill());
     50        rawUrl.setLineWrap(true);
     51        rawUrl.addKeyListener(keyAdapter);
     52
     53        add(new JLabel(tr("{0} Enter maximum zoom (optional)", "3.")), GBC.eol());
     54        mvtZoom.addKeyListener(keyAdapter);
     55        add(mvtZoom, GBC.eop().fill());
     56
     57        add(new JLabel(tr("{0} Edit generated {1} URL (optional)", "4.", "MVT")), GBC.eol());
     58        add(mvtUrl, GBC.eop().fill());
     59        mvtUrl.setLineWrap(true);
     60
     61        add(new JLabel(tr("{0} Enter name for this layer", "5.")), GBC.eol());
     62        add(name, GBC.eop().fill());
     63
     64        registerValidableComponent(mvtUrl);
     65    }
     66
     67    private String buildMvtUrl() {
     68        StringBuilder a = new StringBuilder("mvt");
     69        String z = sanitize(mvtZoom.getText());
     70        if (!z.isEmpty()) {
     71            a.append('[').append(z).append(']');
     72        }
     73        a.append(':').append(sanitize(getImageryRawUrl(), ImageryType.MVT));
     74        return a.toString();
     75    }
     76
     77    @Override
     78    public ImageryInfo getImageryInfo() {
     79        ImageryInfo generated = new ImageryInfo(getImageryName(), getMvtUrl());
     80        generated.setImageryType(ImageryType.MVT);
     81        return generated;
     82    }
     83
     84    protected final String getMvtUrl() {
     85        return sanitize(mvtUrl.getText());
     86    }
     87
     88    @Override
     89    protected boolean isImageryValid() {
     90        return !getImageryName().isEmpty() && !getMvtUrl().isEmpty();
     91    }
     92}
  • src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
    a b  
    312312        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS));
    313313        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS));
    314314        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS));
     315        activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.MVT));
    315316        activeToolbar.add(remove);
    316317        activePanel.add(activeToolbar, BorderLayout.EAST);
    317318        add(activePanel, GBC.eol().fill(GridBagConstraints.BOTH).weight(2.0, 0.4).insets(5, 0, 0, 5));
     
    439440                break;
    440441            case WMTS:
    441442                icon = /* ICON(dialogs/) */ "add_wmts";
     443                break;
     444            case MVT:
     445                icon = /* ICON(dialogs/) */ "add_mvt";
    442446                break;
    443447            default:
    444448                break;
     
    460464            case WMTS:
    461465                p = new AddWMTSLayerPanel();
    462466                break;
     467            case MVT:
     468                p = new AddMVTLayerPanel();
     469                break;
    463470            default:
    464471                throw new IllegalStateException("Type " + type + " not supported");
    465472            }
     
    741748    private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
    742749        URL url;
    743750        try {
    744             url = new URL(eulaUrl.replaceAll("\\{lang\\}", LanguageInfo.getWikiLanguagePrefix()));
     751            url = new URL(eulaUrl.replaceAll("\\{lang}", LanguageInfo.getWikiLanguagePrefix()));
    745752            JosmEditorPane htmlPane;
    746753            try {
    747754                htmlPane = new JosmEditorPane(url);
     
    749756                Logging.trace(e1);
    750757                // give a second chance with a default Locale 'en'
    751758                try {
    752                     url = new URL(eulaUrl.replaceAll("\\{lang\\}", ""));
     759                    url = new URL(eulaUrl.replaceAll("\\{lang}", ""));
    753760                    htmlPane = new JosmEditorPane(url);
    754761                } catch (IOException e2) {
    755762                    Logging.debug(e2);
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.fail;
     7
     8import java.awt.Shape;
     9import java.awt.geom.Ellipse2D;
     10import java.awt.geom.PathIterator;
     11import java.io.File;
     12import java.io.IOException;
     13import java.io.InputStream;
     14import java.nio.file.Paths;
     15import java.text.MessageFormat;
     16import java.util.ArrayList;
     17import java.util.Collection;
     18import java.util.List;
     19import java.util.stream.Collectors;
     20
     21import org.junit.jupiter.api.Test;
     22import org.junit.jupiter.api.extension.RegisterExtension;
     23import org.openstreetmap.josm.TestUtils;
     24import org.openstreetmap.josm.data.coor.LatLon;
     25import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     26import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry;
     27import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     28import org.openstreetmap.josm.data.osm.BBox;
     29import org.openstreetmap.josm.data.osm.DataSet;
     30import org.openstreetmap.josm.data.osm.Node;
     31import org.openstreetmap.josm.data.osm.OsmPrimitive;
     32import org.openstreetmap.josm.data.osm.Relation;
     33import org.openstreetmap.josm.data.osm.RelationMember;
     34import org.openstreetmap.josm.data.osm.Way;
     35import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     36import org.openstreetmap.josm.io.Compression;
     37import org.openstreetmap.josm.testutils.JOSMTestRules;
     38
     39/**
     40 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     41 * @author Taylor Smock
     42 * @since xxx
     43 */
     44class ProtoBufTest {
     45    @RegisterExtension
     46    JOSMTestRules josmTestRules = new JOSMTestRules().preferences();
     47
     48    /**
     49     * Test simple message.
     50     * Check that a simple message is readable
     51     * @throws IOException - if an IO error occurs
     52     */
     53    @Test
     54    void testSimpleMessage() throws IOException {
     55        ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
     56        ProtoBufRecord record = new ProtoBufRecord(parser);
     57        assertEquals(WireType.VARINT, record.getType());
     58        assertEquals(150, record.asUnsignedVarInt().intValue());
     59    }
     60
     61    /**
     62     * Test reading tile from Mapillary ( 14/3251/6258 )
     63     * @throws IOException if there is a problem reading the file
     64     */
     65    @Test
     66    void testRead_14_3251_6258() throws IOException {
     67        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "6258.mvt").toFile();
     68        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     69        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     70        assertEquals(2, records.size());
     71        List<Layer> layers = new ArrayList<>();
     72        for (ProtoBufRecord record : records) {
     73            if (record.getField() == Layer.LAYER_FIELD) {
     74                layers.add(new Layer(record.getBytes()));
     75            } else {
     76                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     77            }
     78        }
     79        Layer mapillarySequences = layers.get(0);
     80        Layer mapillaryPictures = layers.get(1);
     81        assertEquals("mapillary-sequences", mapillarySequences.getName());
     82        assertEquals("mapillary-images", mapillaryPictures.getName());
     83        assertEquals(2048, mapillarySequences.getExtent());
     84        assertEquals(2048, mapillaryPictures.getExtent());
     85
     86        assertEquals(1, mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     87        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).findAny().orElse(null);
     88        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     89        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     90        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     91        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     92        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     93    }
     94
     95    /**
     96     * Test reading tile from OpenInfraMap ( 16/13014/25030 )
     97     * @throws IOException if there is a problem reading the file
     98     */
     99    @Test
     100    void testRead_16_13014_25030() throws IOException {
     101        // TODO finish
     102        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "16", "13014", "25030.pbf").toFile();
     103        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     104        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     105        List<Layer> layers = new ArrayList<>();
     106        for (ProtoBufRecord record : records) {
     107            if (record.getField() == Layer.LAYER_FIELD) {
     108                layers.add(new Layer(record.getBytes()));
     109            } else {
     110                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     111            }
     112        }
     113        assertEquals(19, layers.size());
     114        List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty()).collect(Collectors.toList());
     115        // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
     116        assertEquals(5, dataLayers.size());
     117    }
     118
     119    @Test
     120    void testRead_17_26028_50060() throws IOException {
     121        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "17", "26028", "50060.pbf").toFile();
     122        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     123        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     124        List<Layer> layers = new ArrayList<>();
     125        for (ProtoBufRecord record : records) {
     126            if (record.getField() == Layer.LAYER_FIELD) {
     127                layers.add(new Layer(record.getBytes()));
     128            } else {
     129                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     130            }
     131        }
     132        assertEquals(19, layers.size());
     133        List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty()).collect(Collectors.toList());
     134        // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
     135        assertEquals(5, dataLayers.size());
     136
     137        // power_generator_area was rendered incorrectly
     138        final Layer powerGeneratorArea = dataLayers.stream().filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null);
     139        assertNotNull(powerGeneratorArea);
     140        final int extent = powerGeneratorArea.getExtent();
     141        // 17/26028/50060 bounds
     142        final BBox tileExtent = new BBox(new LatLon(39.068246, -108.511959), new LatLon(39.070381, -108.509219));
     143        final DataSet ds = new DataSet();
     144        for (Geometry feature : powerGeneratorArea.getGeometry()) {
     145            final Collection<OsmPrimitive> primitives = feature.getShapes().stream().flatMap(shape -> convertShape(tileExtent, extent, shape).stream()).collect(Collectors.toList());
     146            primitives.forEach(ds::addPrimitive);
     147            final OsmPrimitive toTag;
     148            if (primitives.size() > 1) {
     149                final Relation relation = new Relation();
     150                primitives.forEach(prim -> relation.addMember(new RelationMember("", prim)));
     151                ds.addPrimitive(relation);
     152                toTag = relation;
     153            } else {
     154                toTag = primitives.iterator().next();
     155            }
     156            feature.getFeature().getTags().forEach((key, value) -> toTag.put(key, value));
     157        }
     158        final Way one = new Way();
     159        one.addNode(new Node(new LatLon(39.0687509, -108.5100816)));
     160        one.addNode(new Node(new LatLon(39.0687509, -108.5095751)));
     161        one.addNode(new Node(new LatLon(39.0687169, -108.5095751)));
     162        one.addNode(new Node(new LatLon(39.0687169, -108.5100816)));
     163        one.addNode(one.getNode(0));
     164        one.setOsmId(666293899, 2);
     165        final BBox searchBBox = one.getBBox();
     166        searchBBox.addPrimitive(one, 0.001);
     167        final Collection<Node> searchedNodes = ds.searchNodes(searchBBox);
     168        OsmDataLayer testLayer = new OsmDataLayer(ds, "", null);
     169        testLayer.autosave(new File("/tmp/test.osm"));
     170        assertEquals(4, searchedNodes.size());
     171    }
     172
     173    /**
     174     * Convert a latlon to a relative latlon for the bbox
     175     * @param tileExtent The tile extent
     176     * @param toConvert The shape
     177     * @return An OSM primitive representing the shape
     178     */
     179    private static Collection<OsmPrimitive> convertShape(BBox tileExtent, int extent, Shape toConvert) {
     180        final List<Node> nodes = new ArrayList<>();
     181        final List<Way> ways = new ArrayList<>();
     182        final List<Relation> relations = new ArrayList<>();
     183        final PathIterator iterator = toConvert.getPathIterator(null);
     184        final List<Node> wayNodes = new ArrayList<>();
     185        while (!iterator.isDone()) {
     186            final double[] coords = new double[6];
     187            final int type = iterator.currentSegment(coords);
     188            if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) {
     189                final Node node = convertPointToNode(tileExtent, extent, coords[0], coords[1]);
     190                nodes.add(node);
     191                if (type == PathIterator.SEG_MOVETO && wayNodes.size() > 1) {
     192                    final Way way = new Way();
     193                    way.setNodes(wayNodes);
     194                    ways.add(way);
     195                    wayNodes.clear();
     196                } else if (type == PathIterator.SEG_MOVETO) {
     197                    wayNodes.clear();
     198                }
     199                wayNodes.add(node);
     200            } else if (type == PathIterator.SEG_CLOSE) {
     201                wayNodes.add(wayNodes.get(0));
     202                final Way way = new Way();
     203                way.setNodes(wayNodes);
     204                ways.add(way);
     205                wayNodes.clear();
     206            }
     207            iterator.next();
     208        }
     209
     210        final Collection<OsmPrimitive> primitives = new ArrayList<>(nodes);
     211        primitives.addAll(ways);
     212        primitives.addAll(relations);
     213        return primitives;
     214    }
     215
     216    private static Node convertPointToNode(BBox tileExtent, int extent, double x, double y) {
     217        final double latDiff = tileExtent.getTopLeftLat() - tileExtent.getBottomRightLat();
     218        final double lonDiff = tileExtent.getBottomRightLon() - tileExtent.getTopLeftLon();
     219        final double lat = tileExtent.getTopLeftLat() - y * latDiff / extent;
     220        final double lon = tileExtent.getTopLeftLon() - x * lonDiff / extent;
     221        return new Node(new LatLon(lat, lon));
     222    }
     223
     224
     225    // TODO remove temporary tests or indicate that they are from the vector-tile-js library (BSD-3)
     226    @Test
     227    void test_14_8801_5371() throws IOException {
     228        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "tmp", "14-8801-5371.vector.pbf").toFile();
     229        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     230        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     231        List<Layer> layers = new ArrayList<>();
     232        for (ProtoBufRecord record : records) {
     233            if (record.getField() == Layer.LAYER_FIELD) {
     234                layers.add(new Layer(record.getBytes()));
     235            } else {
     236                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     237            }
     238        }
     239        assertEquals(20, layers.size());
     240        Geometry park = layers.stream().filter(layer -> "poi_label".equals(layer.getName())).flatMap(layer -> layer.getGeometry().stream()).filter(g -> g.getFeature().getId() == 3000003150561L).findAny().orElse(null);
     241        assertEquals("Mauerpark", park.getFeature().getTags().get("name"));
     242        assertEquals("Park", park.getFeature().getTags().get("type"));
     243
     244        Ellipse2D parkShape = (Ellipse2D) park.getShapes().iterator().next();
     245        assertEquals(3898, parkShape.getCenterX());
     246        assertEquals(1731, parkShape.getCenterY());
     247
     248        Geometry road = layers.stream().filter(layer -> "road".equals(layer.getName())).flatMap(layer -> layer.getGeometry().stream()).skip(656).findFirst().orElse(null);
     249        PathIterator roadIterator = road.getShapes().iterator().next().getPathIterator(null);
     250        double[] coords = new double[6];
     251        assertEquals(PathIterator.SEG_MOVETO, roadIterator.currentSegment(coords));
     252        assertEquals(1988, coords[0]);
     253        assertEquals(306, coords[1]);
     254        roadIterator.next();
     255        assertEquals(PathIterator.SEG_LINETO, roadIterator.currentSegment(coords));
     256        assertEquals(1808, coords[0]);
     257        assertEquals(321, coords[1]);
     258        roadIterator.next();
     259        assertEquals(PathIterator.SEG_LINETO, roadIterator.currentSegment(coords));
     260        assertEquals(1506, coords[0]);
     261        assertEquals(347, coords[1]);
     262    }
     263
     264    @Test
     265    void testSingletonMultiPoint() throws IOException {
     266        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "tmp", "singleton-multi-point.pbf").toFile();
     267        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     268        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     269        List<Layer> layers = new ArrayList<>();
     270        for (ProtoBufRecord record : records) {
     271            if (record.getField() == Layer.LAYER_FIELD) {
     272                layers.add(new Layer(record.getBytes()));
     273            } else {
     274                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     275            }
     276        }
     277        assertEquals(1, layers.size());
     278        assertEquals(1, layers.get(0).getGeometry().size());
     279        Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next();
     280        assertEquals(2059, shape.getCenterX());
     281        assertEquals(2071, shape.getCenterY());
     282    }
     283
     284    @Test
     285    void testReadVarInt() {
     286        assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
     287        assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
     288        assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
     289        // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
     290        Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
     291        assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual, MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE), Long.toBinaryString(actual.longValue())));
     292    }
     293
     294    @Test
     295    void testZigZag() {
     296        assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
     297        assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
     298        assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
     299        assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
     300    }
     301
     302    private Number bytesToVarInt(int... bytes) {
     303        byte[] byteArray = new byte[bytes.length];
     304        for (int i = 0; i < bytes.length; i++) {
     305            byteArray[i] = (byte) bytes[i];
     306        }
     307        return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
     308    }
     309}