Ticket #17177: 17177.2.patch

File 17177.2.patch, 82.9 KB (added by taylor.smock, 3 years ago)

Rendering works on all zoom levels (quirks are present at low zoom levels)

  • 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
  • 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;
     6
     7/**
     8 * An interface that is used to draw vector tiles, instead of using images
     9 * @author Taylor Smock
     10 * @since xxx
     11 */
     12public interface VectorTile {
     13    /**
     14     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     15     * position <code>x</code>/<code>y</code>.
     16     *
     17     * @param g the Graphics object
     18     * @param x x-coordinate in <code>g</code>
     19     * @param y y-coordinate in <code>g</code>
     20     */
     21    void paint(Graphics g, int x, int y);
     22
     23    /**
     24     * Paints the vector tile on the {@link Graphics} <code>g</code> at the
     25     * position <code>x</code>/<code>y</code>.
     26     *  @param g the Graphics object
     27     * @param x x-coordinate in <code>g</code>
     28     * @param y y-coordinate in <code>g</code>
     29     * @param width width that tile should have
     30     * @param height height that tile should have
     31     * @param observer The paint observer. May be {@code null}.
     32     * @param zoom The current zoom level
     33     */
     34    void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer);
     35
     36    /**
     37     * Get the extent of the tile (in pixels)
     38     * @return The tile extent (pixels)
     39     */
     40    int getExtent();
     41}
  • 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            for (CommandInteger command : commands) {
     51                final short[] operations = command.getOperations();
     52                // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior
     53                if (command.getType() == Command.MoveTo && operations.length == 2) {
     54                    final double x;
     55                    final double y;
     56                    if (line != null) {
     57                        x = line.getCurrentPoint().getX() + operations[0];
     58                        y = line.getCurrentPoint().getY() + operations[1];
     59                    } else {
     60                        x = operations[0];
     61                        y = operations[1];
     62                    }
     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 x = line.getCurrentPoint().getX() + operations[2 * i];
     69                        final double y = line.getCurrentPoint().getY() + operations[2 * i + 1];
     70                        line.lineTo(x, y);
     71                    }
     72                // ClosePath should only be used with Polygon geometry
     73                } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {
     74                    line.closePath();
     75                    shapes.remove(line);
     76                    shapes.add(new Area(line));
     77                } else {
     78                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     79                }
     80            }
     81        }
     82    }
     83
     84    /**
     85     * Get the feature for this geometry
     86     * @return The feature
     87     */
     88    public Feature getFeature() {
     89        return this.feature;
     90    }
     91
     92    /**
     93     * Get the shapes to draw this geometry with
     94     * @return A collection of shapes
     95     */
     96    public Collection<Shape> getShapes() {
     97        return Collections.unmodifiableCollection(this.shapes);
     98    }
     99}
  • 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.Collection;
     18import java.util.HashSet;
     19import java.util.function.Function;
     20import java.util.stream.Collectors;
     21
     22import org.openstreetmap.gui.jmapviewer.Tile;
     23import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     24import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     25import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     26import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     27
     28/**
     29 * A class for MapBox Vector Tiles
     30 * @author Taylor Smock
     31 * @since xxx
     32 */
     33public class MVTTile extends Tile implements VectorTile {
     34    private Collection<Geometry> geometry;
     35    private int extent = Layer.DEFAULT_EXTENT;
     36
     37    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
     38        super(source, xtile, ytile, zoom);
     39    }
     40
     41    @Override
     42    public void paint(final Graphics g, final int x, final int y) {
     43        this.paint(g, x, y, 256, 256);
     44    }
     45
     46    @Override
     47    public void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer) {
     48        if (!(g instanceof Graphics2D) || this.geometry == null) {
     49            if (getImage() != null) {
     50                g.drawImage(image, x, y, width, height, observer);
     51            }
     52            return;
     53        }
     54        final Graphics2D graphics = (Graphics2D) g;
     55        graphics.setColor(Color.GREEN);
     56        final AffineTransform originalTransform = graphics.getTransform();
     57        final Stroke originalStroke = graphics.getStroke();
     58        try {
     59            graphics.translate(x, y);
     60            // TODO figure out HiDPI (maybe GuiSizesHelper?)
     61            // 131072 seems to be the magic number for my screens. This needs to be investigated more.
     62            final double scale = width / (double) 131072;
     63            // The scaleTransform is separate to avoid wide lines at high zoom (e.g., when vector tiles go to z14, but
     64            // we are currently at z20, the graphics.scale function makes everything big. Unfortunately, this creates
     65            // a new shape object.
     66            final Function<Shape, Shape> scaleShape;
     67            if (scale > 1) {
     68                final AffineTransform scaleTransform = AffineTransform.getScaleInstance(scale, scale);
     69                scaleShape = scaleTransform::createTransformedShape;
     70            } else {
     71                scaleShape = Function.identity();
     72                graphics.scale(scale, scale);
     73            }
     74            final Color transparentYellow = new Color(Color.YELLOW.getRed(), Color.YELLOW.getGreen(), Color.YELLOW.getBlue(), 120);
     75            this.geometry.forEach(shapes -> {
     76                for (Shape shape : shapes.getShapes()) {
     77                    final Shape scaledShape = scaleShape.apply(shape);
     78                    if (shape instanceof Ellipse2D) {
     79                        graphics.setColor(Color.GREEN);
     80                    } else if (shape instanceof Path2D) {
     81                        graphics.setColor(Color.RED);
     82                    } else if (shape instanceof Area) {
     83                        graphics.setColor(transparentYellow);
     84                        graphics.fill(scaledShape);
     85                        graphics.setColor(Color.YELLOW);
     86                    }
     87                    graphics.draw(scaledShape);
     88                }
     89            });
     90        } finally {
     91            graphics.setTransform(originalTransform);
     92            graphics.setStroke(originalStroke);
     93        }
     94        graphics.setColor(Color.RED);
     95        graphics.drawString("0, 0", 1024, 1024);
     96    }
     97
     98    @Override
     99    public void loadImage(final InputStream inputStream) throws IOException {
     100        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
     101            this.initLoading();
     102            ProtoBufParser parser = new ProtoBufParser(inputStream);
     103            Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
     104            Collection<Layer> layers = new HashSet<>();
     105            for (ProtoBufRecord record : protoBufRecords) {
     106                if (record.getField() == Layer.LAYER_FIELD) {
     107                    Layer mvtLayer = new Layer(new ProtoBufParser(record.getBytes()).allRecords());
     108                    layers.add(mvtLayer);
     109                    // Cleanup bytes
     110                    record.close();
     111                }
     112            }
     113            // TODO Store layers separately
     114            this.geometry = layers.stream().flatMap(layer -> layer.getGeometry().stream()).collect(Collectors.toList());
     115            this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
     116            BufferedImage bufferedImage = new BufferedImage(this.extent, this.extent, BufferedImage.TYPE_4BYTE_ABGR);
     117            Graphics2D graphics = bufferedImage.createGraphics();
     118            this.paint(graphics, 0, 0);
     119
     120            // TODO figure out a better way to free memory
     121            final int maxSize = 256;
     122            final BufferedImage resized = new BufferedImage(maxSize, maxSize, bufferedImage.getType());
     123            resized.getGraphics().drawImage(bufferedImage, 0, 0, resized.getWidth(), resized.getHeight(), null);
     124            this.image = resized;
     125            this.finishLoading();
     126        }
     127    }
     128
     129    @Override
     130    public int getExtent() {
     131        return this.extent;
     132    }
     133}
  • 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}
  • 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        final long value = signed.longValue();
     88        final long number = (value << 1) ^ (value >> 31);
     89        return ProtoBufParser.convertLong(number);
     90    }
     91
     92    /**
     93     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     94     * @return the double
     95     */
     96    public double asDouble() {
     97        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     98        return Double.longBitsToDouble(doubleNumber);
     99    }
     100
     101    /**
     102     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     103     * @return the float
     104     */
     105    public float asFloat() {
     106        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     107        return Float.intBitsToFloat(floatNumber);
     108    }
     109
     110    /**
     111     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     112     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     113     */
     114    public String asString() {
     115        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     116    }
     117
     118    /**
     119     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     120     * @return a byte array of the 32 bits (4 bytes)
     121     */
     122    public byte[] asFixed32() {
     123        // TODO verify, or just assume?
     124        // 4 bytes == 32 bits
     125        return this.bytes;
     126    }
     127
     128    /**
     129     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     130     * @return a byte array of the 64 bits (8 bytes)
     131     */
     132    public byte[] asFixed64() {
     133        // TODO verify, or just assume?
     134        // 8 bytes == 64 bits
     135        return this.bytes;
     136    }
     137
     138    @Override
     139    public void close() {
     140        this.bytes = null;
     141    }
     142}
  • 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;
     
    868869            if (coordinateConverter.requiresReprojection()) {
    869870                tile = new ReprojectionTile(tileSource, x, y, zoom);
    870871            } else {
    871                 tile = new Tile(tileSource, x, y, zoom);
     872                tile = createTile(tileSource, x, y, zoom);
    872873            }
    873874            tileCache.addTile(tile);
    874875        }
     
    10071008        }
    10081009    }
    10091010
     1011    /**
     1012     * Draw a vector tile on screen.
     1013     * @param g the Graphics2D
     1014     * @param tile the vector tile
     1015     * @param anchorImage tile anchor in image coordinates
     1016     * @param anchorScreen tile anchor in screen coordinates
     1017     * @param clip clipping region in screen coordinates (can be null)
     1018     */
     1019    private void drawVectorTileInside(Graphics2D g, VectorTile tile, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
     1020        AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
     1021        Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null);
     1022        Point2D screen1 = imageToScreen.transform(new Point2D.Double(
     1023                tile.getExtent(), tile.getExtent()), null);
     1024
     1025        Shape oldClip = null;
     1026        if (clip != null) {
     1027            oldClip = g.getClip();
     1028            g.clip(clip);
     1029        }
     1030        tile.paint(g, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
     1031                (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
     1032                (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this.currentZoomLevel, this);
     1033        if (clip != null) {
     1034            g.setClip(oldClip);
     1035        }
     1036    }
     1037
    10101038    private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
    10111039        Object paintMutex = new Object();
    10121040        List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
     
    10211049                    img = getLoadedTileImage(tile);
    10221050                    anchorImage = getAnchor(tile, img);
    10231051                }
    1024                 if (img == null || anchorImage == null) {
     1052                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10251053                    miss = true;
    10261054                }
    10271055            }
     
    10301058                return;
    10311059            }
    10321060
    1033             img = applyImageProcessors(img);
     1061            if (img != null) {
     1062                img = applyImageProcessors(img);
     1063            }
    10341064
    10351065            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10361066            synchronized (paintMutex) {
    10371067                //cannot paint in parallel
    1038                 drawImageInside(g, img, anchorImage, anchorScreen, null);
     1068                if (tile instanceof VectorTile) {
     1069                    drawVectorTileInside(g, (VectorTile) tile, anchorImage, anchorScreen, null);
     1070                } else {
     1071                    drawImageInside(g, img, anchorImage, anchorScreen, null);
     1072                }
    10391073            }
    10401074            MapView mapView = MainApplication.getMap().mapView;
    10411075            if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
     
    18301864
    18311865                for (int x = minX; x <= maxX; x++) {
    18321866                    for (int y = minY; y <= maxY; y++) {
    1833                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1867                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18341868                    }
    18351869                }
    18361870            }
     
    19291963        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19301964    }
    19311965
     1966    /**
     1967     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     1968     *
     1969     * @param source Tile source
     1970     * @param x X coordinate
     1971     * @param y Y coordinate
     1972     * @param zoom Zoom level
     1973     * @return The new {@link Tile}
     1974     * @since xxx
     1975     */
     1976    public Tile createTile(T source, int x, int y, int zoom) {
     1977        return new Tile(source, x, y, zoom);
     1978    }
     1979
    19321980    @Override
    19331981    public synchronized void destroy() {
    19341982        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;
     3
     4import java.util.Collection;
     5import java.util.Collections;
     6
     7import org.openstreetmap.gui.jmapviewer.Tile;
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     9import org.openstreetmap.josm.data.imagery.ImageryInfo;
     10import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
     11import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     12import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     13import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     14import org.openstreetmap.josm.data.projection.Projections;
     15import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
     16
     17/**
     18 * A layer for MapBox Vector Tiles
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> {
     23    private static final String CACHE_REGION_NAME = "MVT";
     24
     25    public MVTLayer(ImageryInfo info) {
     26        super(info);
     27    }
     28
     29    @Override
     30    protected Class<? extends TileLoader> getTileLoaderClass() {
     31        return MapBoxVectorCachedTileLoader.class;
     32    }
     33
     34    @Override
     35    protected String getCacheName() {
     36        return CACHE_REGION_NAME;
     37    }
     38
     39    @Override
     40    public Collection<String> getNativeProjections() {
     41        // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
     42        // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
     43        return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
     44    }
     45
     46    @Override
     47    protected MapboxVectorTileSource getTileSource() {
     48        MapboxVectorTileSource source = new MapboxVectorTileSource(this.info);
     49        this.info.setAttribution(source);
     50        return source;
     51    }
     52
     53    @Override
     54    public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
     55        return new MVTTile(source, x, y, zoom);
     56    }
     57
     58}
  • 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            }
  • 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.fail;
     6
     7import java.io.File;
     8import java.io.IOException;
     9import java.io.InputStream;
     10import java.nio.file.Paths;
     11import java.text.MessageFormat;
     12import java.util.ArrayList;
     13import java.util.Collection;
     14import java.util.List;
     15
     16import org.junit.jupiter.api.Test;
     17import org.openstreetmap.josm.TestUtils;
     18import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     19import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     20import org.openstreetmap.josm.io.Compression;
     21
     22/**
     23 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     24 * @author Taylor Smock
     25 * @since xxx
     26 */
     27class ProtoBufTest {
     28    /**
     29     * Test simple message.
     30     * Check that a simple message is readable
     31     * @throws IOException - if an IO error occurs
     32     */
     33    @Test
     34    void testSimpleMessage() throws IOException {
     35        ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
     36        ProtoBufRecord record = new ProtoBufRecord(parser);
     37        assertEquals(WireType.VARINT, record.getType());
     38        assertEquals(150, record.asUnsignedVarInt().intValue());
     39    }
     40
     41    /**
     42     * Test reading tile from Mapillary ( 14/3251/6258 )
     43     * @throws IOException if there is a problem reading the file
     44     */
     45    @Test
     46    void testRead_14_3251_6258() throws IOException {
     47        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "6258.mvt").toFile();
     48        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     49        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     50        assertEquals(2, records.size());
     51        List<Layer> layers = new ArrayList<>();
     52        for (ProtoBufRecord record : records) {
     53            if (record.getField() == Layer.LAYER_FIELD) {
     54                layers.add(new Layer(record.getBytes()));
     55            } else {
     56                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     57            }
     58        }
     59        Layer mapillarySequences = layers.get(0);
     60        Layer mapillaryPictures = layers.get(1);
     61        assertEquals("mapillary-sequences", mapillarySequences.getName());
     62        assertEquals("mapillary-images", mapillaryPictures.getName());
     63        assertEquals(8192, mapillarySequences.getExtent());
     64        assertEquals(8192, mapillaryPictures.getExtent());
     65
     66        assertEquals(1, mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     67        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).findAny().orElse(null);
     68        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     69        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     70        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     71        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     72        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     73    }
     74}