Ticket #17177: 17177.3.patch

File 17177.3.patch, 103.8 KB (added by taylor.smock, 4 years ago)

Fix rendering issue with regards to a multipolygons being incorrectly rendered. This is probably good enough for initial testing (if this patch gets merged, functionality should be hidden behind expert mode).

  • resources/images/dialogs/add_mvt.svg

     
     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

     
    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

     
    3232import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
    3333import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
    3434import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     35import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
    3536import org.openstreetmap.josm.data.preferences.LongProperty;
    3637import org.openstreetmap.josm.tools.HttpClient;
    3738import org.openstreetmap.josm.tools.Logging;
     
    295296            if (content.length > 0) {
    296297                try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
    297298                    tile.loadImage(in);
    298                     if (tile.getImage() == null) {
     299                    if ((!(tile instanceof VectorTile) && tile.getImage() == null)
     300                        || ((tile instanceof VectorTile) && !tile.isLoaded())) {
    299301                        String s = new String(content, StandardCharsets.UTF_8);
    300302                        Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
    301303                        if (m.matches()) {
  • src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java

     
     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;
     9
     10/**
     11 * An interface that is used to draw vector tiles, instead of using images
     12 * @author Taylor Smock
     13 * @since xxx
     14 */
     15public interface VectorTile {
     16    /**
     17     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     18     * position <code>x</code>/<code>y</code>.
     19     *
     20     * @param g the Graphics object
     21     * @param x x-coordinate in <code>g</code>
     22     * @param y y-coordinate in <code>g</code>
     23     */
     24    void paint(Graphics g, int x, int y);
     25
     26    /**
     27     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     28     * position <code>x</code>/<code>y</code>.
     29     *  @param g the Graphics object
     30     * @param x x-coordinate in <code>g</code>
     31     * @param y y-coordinate in <code>g</code>
     32     * @param width width that tile should have
     33     * @param height height that tile should have
     34     * @param observer The paint observer. May be {@code null}.
     35     * @param zoom The current zoom level
     36     */
     37    void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer);
     38
     39    /**
     40     * Get the layers for this vector tile
     41     * @return A collection of layers
     42     */
     43    Collection<Layer> getLayers();
     44
     45    /**
     46     * Get the extent of the tile (in pixels)
     47     * @return The tile extent (pixels)
     48     */
     49    int getExtent();
     50}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java

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

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java

     
     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.TileSource;
     27import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     28import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     29import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     30import org.openstreetmap.josm.tools.ListenerList;
     31import org.openstreetmap.josm.tools.Logging;
     32
     33/**
     34 * A class for MapBox Vector Tiles
     35 * @author Taylor Smock
     36 * @since xxx
     37 */
     38public class MVTTile extends Tile implements VectorTile {
     39    private Collection<Layer> layers;
     40    private int extent = Layer.DEFAULT_EXTENT;
     41    private final ListenerList<TileListener> listenerList = ListenerList.create();
     42    private LayerShower layerShower;
     43
     44    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
     45        super(source, xtile, ytile, zoom);
     46    }
     47
     48    @Override
     49    public void paint(final Graphics g, final int x, final int y) {
     50        this.paint(g, x, y, 256, 256);
     51    }
     52
     53    @Override
     54    public void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer) {
     55        if (!(g instanceof Graphics2D) || this.layers == null) {
     56            if (getImage() != null) {
     57                g.drawImage(image, x, y, width, height, observer);
     58            }
     59            return;
     60        }
     61        final Graphics2D graphics = (Graphics2D) g;
     62        graphics.setColor(Color.GREEN);
     63        final AffineTransform originalTransform = graphics.getTransform();
     64        final Stroke originalStroke = graphics.getStroke();
     65        try {
     66            graphics.translate(x, y);
     67            // TODO figure out HiDPI (maybe GuiSizesHelper?)
     68            // 131072 seems to be the magic number for my screens. This needs to be investigated more.
     69            final double scale = width / (double) (32768);
     70            // The scaleTransform is separate to avoid wide lines at high zoom (e.g., when vector tiles go to z14, but
     71            // we are currently at z20, the graphics.scale function makes everything big. Unfortunately, this creates
     72            // a new shape object.
     73            final UnaryOperator<Shape> scaleShape;
     74            if (scale > 1) {
     75                final AffineTransform scaleTransform = AffineTransform.getScaleInstance(scale, scale);
     76                scaleShape = scaleTransform::createTransformedShape;
     77            } else {
     78                // Cannot use Objects.identity() since it isn't a UnaryOperator
     79                scaleShape = s -> s;
     80                graphics.scale(scale, scale);
     81            }
     82            final Color transparentYellow = new Color(Color.YELLOW.getRed(), Color.YELLOW.getGreen(), Color.YELLOW.getBlue(), 120);
     83            Collection<Layer> layersToShow = new ArrayList<>();
     84            if (this.layerShower != null) {
     85                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);
     86            } else {
     87                layersToShow.addAll(this.layers);
     88            }
     89            for (Layer layer : layersToShow) {
     90                layer.getGeometry().forEach(shapes -> {
     91                    for (Shape shape : shapes.getShapes()) {
     92                        final Shape scaledShape = scaleShape.apply(shape);
     93                        if (shape instanceof Ellipse2D) {
     94                            graphics.setColor(Color.GREEN);
     95                        } else if (shape instanceof Path2D) {
     96                            graphics.setColor(Color.RED);
     97                        } else if (shape instanceof Area) {
     98                            graphics.setColor(transparentYellow);
     99                            graphics.fill(scaledShape);
     100                            graphics.setColor(Color.YELLOW);
     101                        }
     102                        graphics.draw(scaledShape);
     103                    }
     104                });
     105            }
     106        } finally {
     107            graphics.setTransform(originalTransform);
     108            graphics.setStroke(originalStroke);
     109        }
     110        graphics.setColor(Color.RED);
     111        graphics.drawString("0, 0", 1024, 1024);
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java

     
     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}
  • src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java

     
     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}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

     
     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}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

     
     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}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

     
     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}
  • src/org/openstreetmap/josm/data/protobuf/WireType.java

     
     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}
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

     
    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;
     
    869870            if (coordinateConverter.requiresReprojection()) {
    870871                tile = new ReprojectionTile(tileSource, x, y, zoom);
    871872            } else {
    872                 tile = new Tile(tileSource, x, y, zoom);
     873                tile = createTile(tileSource, x, y, zoom);
    873874            }
    874875            tileCache.addTile(tile);
    875876        }
     
    10081009        }
    10091010    }
    10101011
     1012    /**
     1013     * Draw a vector tile on screen.
     1014     * @param g the Graphics2D
     1015     * @param tile the vector tile
     1016     * @param anchorImage tile anchor in image coordinates
     1017     * @param anchorScreen tile anchor in screen coordinates
     1018     * @param clip clipping region in screen coordinates (can be null)
     1019     */
     1020    private void drawVectorTileInside(Graphics2D g, VectorTile tile, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
     1021        AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
     1022        Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null);
     1023        Point2D screen1 = imageToScreen.transform(new Point2D.Double(
     1024                tile.getExtent(), tile.getExtent()), null);
     1025
     1026        Shape oldClip = null;
     1027        if (clip != null) {
     1028            oldClip = g.getClip();
     1029            g.clip(clip);
     1030        }
     1031        tile.paint(g, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
     1032                (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
     1033                (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this.currentZoomLevel, this);
     1034        if (clip != null) {
     1035            g.setClip(oldClip);
     1036        }
     1037    }
     1038
    10111039    private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
    10121040        Object paintMutex = new Object();
    10131041        List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
     
    10221050                    img = getLoadedTileImage(tile);
    10231051                    anchorImage = getAnchor(tile, img);
    10241052                }
    1025                 if (img == null || anchorImage == null) {
     1053                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10261054                    miss = true;
    10271055                }
    10281056            }
     
    10311059                return;
    10321060            }
    10331061
    1034             img = applyImageProcessors(img);
     1062            if (img != null) {
     1063                img = applyImageProcessors(img);
     1064            }
    10351065
    10361066            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10371067            synchronized (paintMutex) {
    10381068                //cannot paint in parallel
    1039                 drawImageInside(g, img, anchorImage, anchorScreen, null);
     1069                if (tile instanceof VectorTile) {
     1070                    drawVectorTileInside(g, (VectorTile) tile, anchorImage, anchorScreen, null);
     1071                } else {
     1072                    drawImageInside(g, img, anchorImage, anchorScreen, null);
     1073                }
    10401074            }
    10411075            MapView mapView = MainApplication.getMap().mapView;
    10421076            if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
     
    18311865
    18321866                for (int x = minX; x <= maxX; x++) {
    18331867                    for (int y = minY; y <= maxY; y++) {
    1834                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1868                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18351869                    }
    18361870                }
    18371871            }
     
    19301964        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19311965    }
    19321966
     1967    /**
     1968     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     1969     *
     1970     * @param source Tile source
     1971     * @param x X coordinate
     1972     * @param y Y coordinate
     1973     * @param zoom Zoom level
     1974     * @return The new {@link Tile}
     1975     * @since xxx
     1976     */
     1977    public Tile createTile(T source, int x, int y, int zoom) {
     1978        return new Tile(source, x, y, zoom);
     1979    }
     1980
    19331981    @Override
    19341982    public synchronized void destroy() {
    19351983        super.destroy();
  • src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

     
    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        }
  • src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java

     
     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.event.ActionEvent;
     7import java.util.ArrayList;
     8import java.util.Arrays;
     9import java.util.Collection;
     10import java.util.Collections;
     11import java.util.HashMap;
     12import java.util.List;
     13import java.util.Map;
     14import java.util.function.BooleanSupplier;
     15import java.util.function.Consumer;
     16import java.util.stream.Collectors;
     17
     18import javax.swing.AbstractAction;
     19import javax.swing.Action;
     20import javax.swing.JCheckBoxMenuItem;
     21
     22import org.openstreetmap.gui.jmapviewer.Tile;
     23import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     24import org.openstreetmap.josm.data.imagery.ImageryInfo;
     25import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     26import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
     27import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     28import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.LayerShower;
     29import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener;
     30import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     31import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     32import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
     33
     34/**
     35 * A layer for MapBox Vector Tiles
     36 * @author Taylor Smock
     37 * @since xxx
     38 */
     39public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements LayerShower, TileListener {
     40    private static final String CACHE_REGION_NAME = "MVT";
     41    private final Map<String, Boolean> layerNames = new HashMap<>();
     42
     43    /**
     44     * Creates an instance of an MVT layer
     45     *
     46     * @param info ImageryInfo describing the layer
     47     */
     48    public MVTLayer(ImageryInfo info) {
     49        super(info);
     50    }
     51
     52    @Override
     53    protected Class<? extends TileLoader> getTileLoaderClass() {
     54        return MapBoxVectorCachedTileLoader.class;
     55    }
     56
     57    @Override
     58    protected String getCacheName() {
     59        return CACHE_REGION_NAME;
     60    }
     61
     62    @Override
     63    public Collection<String> getNativeProjections() {
     64        // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
     65        // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
     66        return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
     67    }
     68
     69    @Override
     70    protected MapboxVectorTileSource getTileSource() {
     71        MapboxVectorTileSource source = new MapboxVectorTileSource(this.info);
     72        this.info.setAttribution(source);
     73        return source;
     74    }
     75
     76    @Override
     77    public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
     78        final MVTTile tile = new MVTTile(source, x, y, zoom);
     79        tile.setLayerShower(this);
     80        tile.addTileLoaderFinisher(this);
     81        return tile;
     82    }
     83
     84    @Override
     85    public Action[] getMenuEntries() {
     86        ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries()));
     87        // Add separator between Info and the layers
     88        actions.add(SeparatorLayerAction.INSTANCE);
     89        for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
     90            actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
     91                    layer -> {layerNames.compute(layer, (key, value) -> !value); this.invalidate(); }));
     92        }
     93        return actions.toArray(new Action[0]);
     94    }
     95
     96    private static class EnableLayerAction extends AbstractAction implements LayerAction {
     97        private final String layer;
     98        private final Consumer<String> consumer;
     99        private final BooleanSupplier state;
     100        public EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) {
     101            super(tr("Toggle layer {0}", layer));
     102            this.layer = layer;
     103            this.consumer = consumer;
     104            this.state = state;
     105        }
     106        @Override
     107        public void actionPerformed(ActionEvent e) {
     108            consumer.accept(layer);
     109        }
     110        @Override
     111        public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
     112            return layers.stream().allMatch(MVTLayer.class::isInstance);
     113        }
     114        @Override
     115        public Component createMenuComponent() {
     116            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     117            item.setSelected(this.state.getAsBoolean());
     118            return item;
     119        }
     120    }
     121
     122    @Override
     123    public void finishedLoading(MVTTile tile) {
     124        for (Layer layer : tile.getLayers()) {
     125            this.layerNames.computeIfAbsent(layer.getName(), key -> true);
     126        }
     127    }
     128
     129    @Override
     130    public List<String> layersToShow() {
     131        return this.layerNames.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList());
     132    }
     133}
  • src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java

     
     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

     
    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));
     
    440441            case WMTS:
    441442                icon = /* ICON(dialogs/) */ "add_wmts";
    442443                break;
     444            case MVT:
     445                icon = /* ICON(dialogs/) */ "add_mvt";
     446                break;
    443447            default:
    444448                break;
    445449            }
     
    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);
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

     
     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}