Ticket #17177: 17177.9.patch

File 17177.9.patch, 370.7 KB (added by taylor.smock, 5 years ago)
  • new file resources/images/dialogs/add_mvt.svg

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

    diff --git src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
    index a8561a771..eeac761c6 100644
     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.cache;
    33
     4import java.io.File;
    45import java.io.FileNotFoundException;
    56import java.io.IOException;
     7import java.io.InputStream;
    68import java.net.HttpURLConnection;
    79import java.net.URL;
     10import java.nio.file.Files;
    811import java.security.SecureRandom;
    912import java.util.Collections;
    1013import java.util.List;
    import java.util.concurrent.ThreadPoolExecutor;  
    1720import java.util.concurrent.TimeUnit;
    1821import java.util.regex.Matcher;
    1922
    20 import org.apache.commons.jcs3.access.behavior.ICacheAccess;
    21 import org.apache.commons.jcs3.engine.behavior.ICacheElement;
    2223import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
    2324import org.openstreetmap.josm.data.imagery.TileJobOptions;
    2425import org.openstreetmap.josm.data.preferences.IntegerProperty;
    import org.openstreetmap.josm.tools.HttpClient;  
    2728import org.openstreetmap.josm.tools.Logging;
    2829import org.openstreetmap.josm.tools.Utils;
    2930
     31import org.apache.commons.compress.utils.IOUtils;
     32import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     33import org.apache.commons.jcs3.engine.behavior.ICacheElement;
     34
    3035/**
    3136 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
    3237 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    294299        if (attributes == null) {
    295300            attributes = new CacheEntryAttributes();
    296301        }
     302        final URL url = this.getUrlNoException();
     303        if (url == null) {
     304            return false;
     305        }
     306
     307        if (url.getProtocol().contains("http")) {
     308            return loadObjectHttp();
     309        }
     310        if (url.getProtocol().contains("file")) {
     311            return loadObjectFile(url);
     312        }
     313
     314        return false;
     315    }
     316
     317    private boolean loadObjectFile(URL url) {
     318        String fileName = url.toExternalForm();
     319        File file = new File(fileName.substring("file:/".length() - 1));
     320        if (!file.exists()) {
     321            file = new File(fileName.substring("file://".length() - 1));
     322        }
     323        try (InputStream fileInputStream = Files.newInputStream(file.toPath())) {
     324            cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream));
     325            cache.put(getCacheKey(), cacheData, attributes);
     326            return true;
     327        } catch (IOException e) {
     328            Logging.error(e);
     329            attributes.setError(e);
     330            attributes.setException(e);
     331        }
     332        return false;
     333    }
     334
     335    /**
     336     * @return true if object was successfully downloaded via http, false, if there was a loading failure
     337     */
     338    private boolean loadObjectHttp() {
    297339        try {
    298340            // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
    299341            // then just use HEAD request and check returned values
    public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements  
    553595        try {
    554596            return getUrl();
    555597        } catch (IOException e) {
     598            Logging.trace(e);
    556599            return null;
    557600        }
    558601    }
  • src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    diff --git src/org/openstreetmap/josm/data/imagery/ImageryInfo.java src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
    index 07cabc76a..32b1055ed 100644
    public class ImageryInfo extends  
    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
    public class ImageryInfo extends  
    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;
    public class ImageryInfo extends  
    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

    diff --git src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
    index 41c69e377..358e2d810 100644
    import java.net.URL;  
    1010import java.nio.charset.StandardCharsets;
    1111import java.util.HashSet;
    1212import java.util.List;
     13import java.util.Locale;
    1314import java.util.Map;
    1415import java.util.Map.Entry;
    1516import java.util.Optional;
    import java.util.concurrent.TimeUnit;  
    2122import java.util.regex.Matcher;
    2223import java.util.regex.Pattern;
    2324
    24 import org.apache.commons.jcs3.access.behavior.ICacheAccess;
    2525import org.openstreetmap.gui.jmapviewer.Tile;
    2626import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
    2727import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    import org.openstreetmap.josm.data.cache.CacheEntry;  
    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;
     36import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
    3537import org.openstreetmap.josm.data.preferences.LongProperty;
    3638import org.openstreetmap.josm.tools.HttpClient;
    3739import org.openstreetmap.josm.tools.Logging;
    3840import org.openstreetmap.josm.tools.Utils;
    3941
     42import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     43
    4044/**
    4145 * Class bridging TMS requests to JCS cache requests
    4246 *
    public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe  
    147151    private boolean isNotImage(Map<String, List<String>> headers, int statusCode) {
    148152        if (statusCode == 200 && headers.containsKey("Content-Type") && !headers.get("Content-Type").isEmpty()) {
    149153            String contentType = headers.get("Content-Type").stream().findAny().get();
    150             if (contentType != null && !contentType.startsWith("image")) {
     154            if (contentType != null && !contentType.startsWith("image") && !MVTFile.MIMETYPE.contains(contentType.toLowerCase(Locale.ROOT))) {
    151155                Logging.warn("Image not returned for tile: " + url + " content type was: " + contentType);
    152156                // not an image - do not store response in cache, so next time it will be queried again from the server
    153157                return true;
    public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe  
    318322    private boolean tryLoadTileImage(CacheEntry object) throws IOException {
    319323        if (object != null) {
    320324            byte[] content = object.getContent();
    321             if (content.length > 0) {
     325            if (content.length > 0 || tile instanceof VectorTile) {
    322326                try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
    323327                    tile.loadImage(in);
    324                     if (tile.getImage() == null) {
     328                    if ((!(tile instanceof VectorTile) && tile.getImage() == null)
     329                        || ((tile instanceof VectorTile) && !tile.isLoaded())) {
    325330                        String s = new String(content, StandardCharsets.UTF_8);
    326331                        Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
    327332                        if (m.matches()) {
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
    new file mode 100644
    index 000000000..692f3ea8c
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile;
     3
     4import java.util.Collection;
     5
     6import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     7
     8/**
     9 * An interface that is used to draw vector tiles, instead of using images
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public interface VectorTile {
     14    /**
     15     * Get the layers for this vector tile
     16     * @return A collection of layers
     17     */
     18    Collection<Layer> getLayers();
     19
     20    /**
     21     * Get the extent of the tile (in pixels)
     22     * @return The tile extent (pixels)
     23     */
     24    int getExtent();
     25}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java

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

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

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

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
    new file mode 100644
    index 000000000..c612c7e83
    - +  
     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.ArrayList;
     11import java.util.Collection;
     12import java.util.Collections;
     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    final Collection<Shape> shapes = new ArrayList<>();
     22
     23    /**
     24     * Create a {@link Geometry} for a {@link Feature}
     25     * @param geometryType The type of geometry
     26     * @param commands The commands used to create the geometry
     27     */
     28    public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) {
     29        if (geometryType == GeometryTypes.POINT) {
     30            for (CommandInteger command : commands) {
     31                final short[] operations = command.getOperations();
     32                // Each MoveTo command is a new point
     33                if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) {
     34                    for (int i = 0; i < operations.length / 2; i++) {
     35                        // Just using Ellipse2D since it extends Shape
     36                        shapes.add(new Ellipse2D.Float(operations[2 * i],
     37                                operations[2 * i + 1], 0, 0));
     38                    }
     39                } else {
     40                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     41                }
     42            }
     43        } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) {
     44            Path2D.Float line = null;
     45            Area area = null;
     46            // MVT uses delta encoding. Each feature starts at (0, 0).
     47            double x = 0;
     48            double y = 0;
     49            // Area is used to determine the inner/outer of a polygon
     50            double areaAreaSq = 0;
     51            for (CommandInteger command : commands) {
     52                final short[] operations = command.getOperations();
     53                // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior
     54                if (command.getType() == Command.MoveTo && operations.length == 2) {
     55                    areaAreaSq = 0;
     56                    x += operations[0];
     57                    y += operations[1];
     58                    line = new Path2D.Float();
     59                    line.moveTo(x, y);
     60                    shapes.add(line);
     61                } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) {
     62                    for (int i = 0; i < operations.length / 2; i++) {
     63                        final double lx = x;
     64                        final double ly = y;
     65                        x += operations[2 * i];
     66                        y += operations[2 * i + 1];
     67                        areaAreaSq += lx * y - x * ly;
     68                        line.lineTo(x, y);
     69                    }
     70                // ClosePath should only be used with Polygon geometry
     71                } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {
     72                    shapes.remove(line);
     73                    // new Area() closes the line if it isn't already closed
     74                    if (area == null) {
     75                        area = new Area();
     76                        shapes.add(area);
     77                    }
     78
     79                    Area nArea = new Area(line);
     80                    // SonarLint thinks that this is never > 0. It can be.
     81                    if (areaAreaSq > 0) {
     82                        area.add(nArea);
     83                    } else if (areaAreaSq < 0) {
     84                        area.exclusiveOr(nArea);
     85                    } else {
     86                        throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType));
     87                    }
     88                } else {
     89                    throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
     90                }
     91            }
     92        }
     93    }
     94
     95    /**
     96     * Get the shapes to draw this geometry with
     97     * @return A collection of shapes
     98     */
     99    public Collection<Shape> getShapes() {
     100        return Collections.unmodifiableCollection(this.shapes);
     101    }
     102}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
    new file mode 100644
    index 000000000..0dc29c6a6
    - +  
     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,
     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,
     15    /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */
     16    LINESTRING,
     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;
     20
     21    /**
     22     * Rings used by {@link GeometryTypes#POLYGON}
     23     * @author Taylor Smock
     24     */
     25    public enum Ring {
     26        /** A ring that goes in the clockwise direction */
     27        ExteriorRing,
     28        /** A ring that goes in the anti-clockwise direction */
     29        InteriorRing
     30    }
     31}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java

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

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
    new file mode 100644
    index 000000000..09851e8c7
    - +  
     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 final 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        /**
     38         * A collection of methods to map a record to a type
     39         */
     40        public static final Collection<ValueFields<?>> MAPPERS =
     41          Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL));
     42
     43        private final byte field;
     44        private final Function<ProtoBufRecord, T> conversion;
     45        private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
     46            this.field = (byte) field;
     47            this.conversion = conversion;
     48        }
     49
     50        /**
     51         * Get the field identifier for the value
     52         * @return The identifier
     53         */
     54        public byte getField() {
     55            return this.field;
     56        }
     57
     58        /**
     59         * Convert a protobuf record to a value
     60         * @param protobufRecord The record to convert
     61         * @return the converted value
     62         */
     63        public T convertValue(ProtoBufRecord protobufRecord) {
     64            return this.conversion.apply(protobufRecord);
     65        }
     66    }
     67
     68    /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
     69    public static final byte LAYER_FIELD = 3;
     70    private static final byte VERSION_FIELD = 15;
     71    private static final byte NAME_FIELD = 1;
     72    private static final byte FEATURE_FIELD = 2;
     73    private static final byte KEY_FIELD = 3;
     74    private static final byte VALUE_FIELD = 4;
     75    private static final byte EXTENT_FIELD = 5;
     76    /** The default extent for a vector tile */
     77    static final int DEFAULT_EXTENT = 4096;
     78    private static final byte DEFAULT_VERSION = 1;
     79    /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
     80    private final byte version;
     81    /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
     82    private final String name;
     83
     84    /** The extent of the tile, typically 4096. Required. */
     85    private final int extent;
     86
     87    /** A list of unique keys. Order is important. Optional. */
     88    private final List<String> keyList = new ArrayList<>();
     89    /** A list of unique values. Order is important. Optional. */
     90    private final List<Object> valueList = new ArrayList<>();
     91    /** The actual features of this layer in this tile */
     92    private final List<Feature> featureCollection;
     93
     94    /**
     95     * Create a layer from a collection of records
     96     * @param records The records to convert to a layer
     97     * @throws IOException - if an IO error occurs
     98     */
     99    public Layer(Collection<ProtoBufRecord> records) throws IOException {
     100        // Do the unique required fields first
     101        Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
     102        this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream()
     103          .map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION);
     104        // Per spec, we cannot continue past this until we have checked the version number
     105        if (this.version != 1 && this.version != 2) {
     106            throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
     107        }
     108        this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
     109                .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
     110        this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt)
     111                .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
     112
     113        sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
     114                .forEachOrdered(this.keyList::add);
     115        sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes)
     116                .map(ProtoBufParser::new).map(parser1 -> {
     117                    try {
     118                        return new ProtoBufRecord(parser1);
     119                    } catch (IOException e) {
     120                        Logging.error(e);
     121                        return null;
     122                    }
     123                })
     124                .filter(Objects::nonNull)
     125                .map(value -> ValueFields.MAPPERS.parallelStream()
     126                        .filter(v -> v.getField() == value.getField())
     127                        .map(v -> v.convertValue(value)).findFirst()
     128                        .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField()))))
     129                .forEachOrdered(this.valueList::add);
     130        Collection<IOException> exceptions = new HashSet<>(0);
     131        this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> {
     132            try {
     133                return new Feature(this, feature);
     134            } catch (IOException e) {
     135                exceptions.add(e);
     136            }
     137            return null;
     138        }).collect(Collectors.toList());
     139        if (!exceptions.isEmpty()) {
     140            throw exceptions.iterator().next();
     141        }
     142        // Cleanup bytes (for memory)
     143        for (ProtoBufRecord record : records) {
     144            record.close();
     145        }
     146    }
     147
     148    /**
     149     * Get all the records from a array of bytes
     150     * @param bytes The byte information
     151     * @return All the protobuf records
     152     * @throws IOException If there was an error reading the bytes (unlikely)
     153     */
     154    private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException {
     155        try (ProtoBufParser parser = new ProtoBufParser(bytes)) {
     156            return parser.allRecords();
     157        }
     158    }
     159
     160    /**
     161     * Create a new layer
     162     * @param bytes The bytes that the layer comes from
     163     * @throws IOException - if an IO error occurs
     164     */
     165    public Layer(byte[] bytes) throws IOException {
     166        this(getAllRecords(bytes));
     167    }
     168
     169    /**
     170     * Get the extent of the tile
     171     * @return The layer extent
     172     */
     173    public int getExtent() {
     174        return this.extent;
     175    }
     176
     177    /**
     178     * Get the feature on this layer
     179     * @return the features
     180     */
     181    public Collection<Feature> getFeatures() {
     182        return Collections.unmodifiableCollection(this.featureCollection);
     183    }
     184
     185    /**
     186     * Get the geometry for this layer
     187     * @return The geometry
     188     */
     189    public Collection<Geometry> getGeometry() {
     190        return getFeatures().stream().map(Feature::getGeometryObject).collect(Collectors.toList());
     191    }
     192
     193    /**
     194     * Get a specified key
     195     * @param index The index in the key list
     196     * @return The actual key
     197     */
     198    public String getKey(int index) {
     199        return this.keyList.get(index);
     200    }
     201
     202    /**
     203     * Get the name of the layer
     204     * @return The layer name
     205     */
     206    public String getName() {
     207        return this.name;
     208    }
     209
     210    /**
     211     * Get a specified value
     212     * @param index The index in the value list
     213     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
     214     */
     215    public Object getValue(int index) {
     216        return this.valueList.get(index);
     217    }
     218
     219    /**
     220     * Get the MapBox Vector Tile version specification for this layer
     221     * @return The version of the MapBox Vector Tile specification
     222     */
     223    public byte getVersion() {
     224        return this.version;
     225    }
     226
     227    @Override
     228    public boolean equals(Object other) {
     229        if (other instanceof Layer) {
     230            Layer o = (Layer) other;
     231            return this.extent == o.extent
     232              && this.version == o.version
     233              && Objects.equals(this.name, o.name)
     234              && Objects.equals(this.featureCollection, o.featureCollection)
     235              && Objects.equals(this.keyList, o.keyList)
     236              && Objects.equals(this.valueList, o.valueList);
     237        }
     238        return false;
     239    }
     240
     241    @Override
     242    public int hashCode() {
     243        return Objects.hash(this.name, this.version, this.extent, this.featureCollection, this.keyList, this.valueList);
     244    }
     245}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java

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

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
    new file mode 100644
    index 000000000..5d1d781dd
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.awt.image.BufferedImage;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.util.Collection;
     8import java.util.HashSet;
     9import java.util.List;
     10import java.util.stream.Collectors;
     11
     12import org.openstreetmap.gui.jmapviewer.Tile;
     13import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     14import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     15import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     16import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     17import org.openstreetmap.josm.tools.ListenerList;
     18import org.openstreetmap.josm.tools.Logging;
     19
     20/**
     21 * A class for MapBox Vector Tiles
     22 *
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26public class MVTTile extends Tile implements VectorTile {
     27    private final ListenerList<TileListener> listenerList = ListenerList.create();
     28    private Collection<Layer> layers;
     29    private int extent = Layer.DEFAULT_EXTENT;
     30    static final BufferedImage CLEAR_LOADED = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
     31
     32    /**
     33     * Create a new Tile
     34     * @param source The source of the tile
     35     * @param xtile The x coordinate for the tile
     36     * @param ytile The y coordinate for the tile
     37     * @param zoom The zoom for the tile
     38     */
     39    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
     40        super(source, xtile, ytile, zoom);
     41    }
     42
     43    @Override
     44    public void loadImage(final InputStream inputStream) throws IOException {
     45        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
     46            this.initLoading();
     47            ProtoBufParser parser = new ProtoBufParser(inputStream);
     48            Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
     49            this.layers = new HashSet<>();
     50            this.layers = protoBufRecords.stream().map(record -> {
     51                Layer mvtLayer = null;
     52                if (record.getField() == Layer.LAYER_FIELD) {
     53                    try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) {
     54                        mvtLayer = new Layer(tParser.allRecords());
     55                    } catch (IOException e) {
     56                        Logging.error(e);
     57                    } finally {
     58                        // Cleanup bytes
     59                        record.close();
     60                    }
     61                }
     62                return mvtLayer;
     63            }).collect(Collectors.toCollection(HashSet::new));
     64            this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
     65            this.finishLoading();
     66            this.listenerList.fireEvent(event -> event.finishedLoading(this));
     67            // Ensure that we don't keep the loading image around
     68            this.image = CLEAR_LOADED;
     69        }
     70    }
     71
     72    @Override
     73    public Collection<Layer> getLayers() {
     74        return this.layers;
     75    }
     76
     77    @Override
     78    public int getExtent() {
     79        return this.extent;
     80    }
     81
     82    /**
     83     * Add a tile loader finisher listener
     84     *
     85     * @param listener The listener to add
     86     */
     87    public void addTileLoaderFinisher(TileListener listener) {
     88        // Add as weak listeners since we don't want to keep unnecessary references.
     89        this.listenerList.addWeakListener(listener);
     90    }
     91
     92    /**
     93     * A class that can be notified that a tile has finished loading
     94     *
     95     * @author Taylor Smock
     96     */
     97    public interface TileListener {
     98        /**
     99         * Called when the MVTTile is finished loading
     100         *
     101         * @param tile The tile that finished loading
     102         */
     103        void finishedLoading(MVTTile tile);
     104    }
     105
     106    /**
     107     * A class used to set the layers that an MVTTile will show.
     108     *
     109     * @author Taylor Smock
     110     */
     111    public interface LayerShower {
     112        /**
     113         * Get a list of layers to show
     114         *
     115         * @return A list of layer names
     116         */
     117        List<String> layersToShow();
     118    }
     119}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java

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

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
    new file mode 100644
    index 000000000..748172f5f
    - +  
     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.openstreetmap.gui.jmapviewer.Tile;
     7import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     8import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     9import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
     10import org.openstreetmap.josm.data.imagery.TileJobOptions;
     11
     12import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     13
     14/**
     15 * Bridge to JCS cache for MVT tiles
     16 * @author Taylor Smock
     17 * @since xxx
     18 */
     19public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
     20
     21    public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
     22            ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
     23            ThreadPoolExecutor downloadExecutor) {
     24        super(listener, tile, cache, options, downloadExecutor);
     25    }
     26}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
    new file mode 100644
    index 000000000..413c7b32b
    - +  
     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.io.InputStream;
     7import java.util.List;
     8import java.util.Objects;
     9import java.util.stream.Collectors;
     10
     11import javax.json.Json;
     12import javax.json.JsonException;
     13import javax.json.JsonReader;
     14
     15import org.openstreetmap.josm.data.imagery.ImageryInfo;
     16import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
     17import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
     18import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
     19import org.openstreetmap.josm.gui.ExtendedDialog;
     20import org.openstreetmap.josm.gui.MainApplication;
     21import org.openstreetmap.josm.gui.util.GuiHelper;
     22import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     23import org.openstreetmap.josm.io.CachedFile;
     24import org.openstreetmap.josm.tools.Logging;
     25
     26/**
     27 * Tile Source handling for Mapbox Vector Tile sources
     28 * @author Taylor Smock
     29 * @since xxx
     30 */
     31public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
     32    private final MapBoxVectorStyle styleSource;
     33
     34    /**
     35     * Create a new {@link MapboxVectorTileSource} from an {@link ImageryInfo}
     36     * @param info The info to create the source from
     37     */
     38    public MapboxVectorTileSource(ImageryInfo info) {
     39        super(info);
     40        MapBoxVectorStyle mapBoxVectorStyle = null;
     41        try (CachedFile style = new CachedFile(info.getUrl());
     42          InputStream inputStream = style.getInputStream();
     43          JsonReader reader = Json.createReader(inputStream)) {
     44            reader.readObject();
     45            // OK, we have a stylesheet
     46            mapBoxVectorStyle = MapBoxVectorStyle.getMapBoxVectorStyle(info.getUrl());
     47        } catch (IOException | JsonException e) {
     48            Logging.trace(e);
     49        }
     50        this.styleSource = mapBoxVectorStyle;
     51        if (this.styleSource != null) {
     52            final Source source;
     53            List<Source> sources = this.styleSource.getSources().keySet().stream().filter(Objects::nonNull)
     54              .collect(Collectors.toList());
     55            if (sources.size() == 1) {
     56                source = sources.get(0);
     57            } else if (!sources.isEmpty()) {
     58                // Ask user what source they want.
     59                source = GuiHelper.runInEDTAndWaitAndReturn(() -> {
     60                    ExtendedDialog dialog = new ExtendedDialog(MainApplication.getMainFrame(),
     61                      tr("Select Vector Tile Layers"), tr("Add layers"));
     62                    JosmComboBox<Source> comboBox = new JosmComboBox<>(sources.toArray(new Source[0]));
     63                    comboBox.setSelectedIndex(0);
     64                    dialog.setContent(comboBox);
     65                    dialog.showDialog();
     66                    return (Source) comboBox.getSelectedItem();
     67                });
     68            } else {
     69                // Umm. What happened? We probably have an invalid style source.
     70                throw new InvalidMapboxVectorTileException(tr("Cannot understand style source: {0}", info.getUrl()));
     71            }
     72            if (source != null) {
     73                this.name = name + ": " + source.getName();
     74                // There can technically be multiple URL's for this field; unfortunately, JOSM can only handle one right now.
     75                this.baseUrl = source.getUrls().get(0);
     76                this.minZoom = source.getMinZoom();
     77                this.maxZoom = source.getMaxZoom();
     78                if (source.getAttributionText() != null) {
     79                    this.setAttributionText(source.getAttributionText());
     80                }
     81            }
     82        }
     83    }
     84
     85    /**
     86     * Get the style source for this Vector Tile source
     87     * @return The source to use for styling
     88     */
     89    public MapBoxVectorStyle getStyleSource() {
     90        return this.styleSource;
     91    }
     92}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
    new file mode 100644
    index 000000000..a7f677755
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import java.util.Arrays;
     5import java.util.Objects;
     6import java.util.stream.Collectors;
     7
     8import javax.json.JsonArray;
     9import javax.json.JsonObject;
     10import javax.json.JsonString;
     11import javax.json.JsonValue;
     12
     13/**
     14 * A MapBox vector style expression (immutable)
     15 * @author Taylor Smock
     16 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a>
     17 * @since xxx
     18 */
     19public final class Expression {
     20    /** An empty expression to use */
     21    public static final Expression EMPTY_EXPRESSION = new Expression(JsonValue.NULL);
     22    private static final String EMPTY_STRING = "";
     23
     24    private final String mapcssFilterExpression;
     25
     26    /**
     27     * Create a new filter expression. <i>Please note that this currently only supports basic comparators!</i>
     28     * @param value The value to parse
     29     */
     30    public Expression(JsonValue value) {
     31        if (value.getValueType() == JsonValue.ValueType.ARRAY) {
     32            final JsonArray array = value.asJsonArray();
     33            if (!array.isEmpty() && array.get(0).getValueType() == JsonValue.ValueType.STRING) {
     34                if ("==".equals(array.getString(0))) {
     35                    // The mapcss equivalent of == is = (for the most part)
     36                    this.mapcssFilterExpression = convertToString(array.get(1)) + "=" + convertToString(array.get(2));
     37                } else if (Arrays.asList("<=", ">=", ">", "<", "!=").contains(array.getString(0))) {
     38                    this.mapcssFilterExpression = convertToString(array.get(1)) + array.getString(0) + convertToString(array.get(2));
     39                } else {
     40                    this.mapcssFilterExpression = EMPTY_STRING;
     41                }
     42            } else {
     43                this.mapcssFilterExpression = EMPTY_STRING;
     44            }
     45        } else {
     46            this.mapcssFilterExpression = EMPTY_STRING;
     47        }
     48    }
     49
     50    /**
     51     * Convert a value to a string
     52     * @param value The value to convert
     53     * @return A string
     54     */
     55    private static String convertToString(JsonValue value) {
     56        switch (value.getValueType()) {
     57        case STRING:
     58            return ((JsonString) value).getString();
     59        case FALSE:
     60            return Boolean.FALSE.toString();
     61        case TRUE:
     62            return Boolean.TRUE.toString();
     63        case NUMBER:
     64            return value.toString();
     65        case ARRAY:
     66            return '['
     67              + ((JsonArray) value).stream().map(Expression::convertToString).collect(Collectors.joining(","))
     68              + ']';
     69        case OBJECT:
     70            return '{'
     71              + ((JsonObject) value).entrySet().stream()
     72              .map(entry -> entry.getKey() + ":" + convertToString(entry.getValue())).collect(
     73                Collectors.joining(","))
     74              + '}';
     75        case NULL:
     76        default:
     77            return EMPTY_STRING;
     78        }
     79    }
     80
     81    @Override
     82    public String toString() {
     83        return !EMPTY_STRING.equals(this.mapcssFilterExpression) ? '[' + this.mapcssFilterExpression + ']' : EMPTY_STRING;
     84    }
     85
     86    @Override
     87    public boolean equals(Object other) {
     88        if (other instanceof Expression) {
     89            Expression o = (Expression) other;
     90            return Objects.equals(this.mapcssFilterExpression, o.mapcssFilterExpression);
     91        }
     92        return false;
     93    }
     94
     95    @Override
     96    public int hashCode() {
     97        return Objects.hash(this.mapcssFilterExpression);
     98    }
     99}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
    new file mode 100644
    index 000000000..9488c3d19
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import java.awt.Font;
     5import java.awt.GraphicsEnvironment;
     6import java.text.MessageFormat;
     7import java.util.Arrays;
     8import java.util.Collection;
     9import java.util.List;
     10import java.util.Locale;
     11import java.util.Objects;
     12import java.util.regex.Matcher;
     13import java.util.regex.Pattern;
     14import java.util.stream.Collectors;
     15import java.util.stream.Stream;
     16
     17import javax.json.JsonArray;
     18import javax.json.JsonNumber;
     19import javax.json.JsonObject;
     20import javax.json.JsonString;
     21import javax.json.JsonValue;
     22
     23/**
     24 * MapBox style layers
     25 * @author Taylor Smock
     26 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/">https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/</a>
     27 * @since xxx
     28 */
     29public class Layers {
     30    /**
     31     * The layer type. This affects the rendering.
     32     * @author Taylor Smock
     33     * @since xxx
     34     */
     35    enum Type {
     36        /** Filled polygon with an (optional) border */
     37        FILL,
     38        /** A line */
     39        LINE,
     40        /** A symbol */
     41        SYMBOL,
     42        /** A circle */
     43        CIRCLE,
     44        /** A heatmap */
     45        HEATMAP,
     46        /** A 3D polygon extrusion */
     47        FILL_EXTRUSION,
     48        /** Raster */
     49        RASTER,
     50        /** Hillshade data */
     51        HILLSHADE,
     52        /** A background color or pattern */
     53        BACKGROUND,
     54        /** The fallback layer */
     55        SKY
     56    }
     57
     58    private static final String EMPTY_STRING = "";
     59    private static final char SEMI_COLON = ';';
     60    private static final Pattern CURLY_BRACES = Pattern.compile("(\\{(.*?)})");
     61
     62    /** A required unique layer name */
     63    private final String id;
     64    /** The required type */
     65    private final Type type;
     66    /** An optional expression */
     67    private final Expression filter;
     68    /** The max zoom for the layer */
     69    private final int maxZoom;
     70    /** The min zoom for the layer */
     71    private final int minZoom;
     72
     73    /** Default paint properties for this layer */
     74    private final String paint;
     75
     76    /** A source description to be used with this layer. Required for everything <i>but</i> {@link Type#BACKGROUND} */
     77    private final String source;
     78    /** Layer to use from the vector tile source. Only allowed with {@link SourceType#VECTOR}. */
     79    private final String sourceLayer;
     80    /** The id for the style -- used for image paths */
     81    private final String styleId;
     82    /**
     83     * Create a layer object
     84     * @param layerInfo The info to use to create the layer
     85     */
     86    public Layers(final JsonObject layerInfo) {
     87        this (null, layerInfo);
     88    }
     89
     90    /**
     91     * Create a layer object
     92     * @param styleId The id for the style (image paths require this)
     93     * @param layerInfo The info to use to create the layer
     94     */
     95    public Layers(final String styleId, final JsonObject layerInfo) {
     96        this.id = layerInfo.getString("id");
     97        this.styleId = styleId;
     98        this.type = Type.valueOf(layerInfo.getString("type").replace("-", "_").toUpperCase(Locale.ROOT));
     99        if (layerInfo.containsKey("filter")) {
     100            this.filter = new Expression(layerInfo.get("filter"));
     101        } else {
     102            this.filter = Expression.EMPTY_EXPRESSION;
     103        }
     104        this.maxZoom = layerInfo.getInt("maxzoom", Integer.MAX_VALUE);
     105        this.minZoom = layerInfo.getInt("minzoom", Integer.MIN_VALUE);
     106        // There is a metadata field (I don't *think* I need it?)
     107        // source is only optional with {@link Type#BACKGROUND}.
     108        if (this.type == Type.BACKGROUND) {
     109            this.source = layerInfo.getString("source", null);
     110        } else {
     111            this.source = layerInfo.getString("source");
     112        }
     113        if (layerInfo.containsKey("paint") && layerInfo.get("paint").getValueType() == JsonValue.ValueType.OBJECT) {
     114            final JsonObject paintObject = layerInfo.getJsonObject("paint");
     115            final JsonObject layoutObject = layerInfo.getOrDefault("layout", JsonValue.EMPTY_JSON_OBJECT).asJsonObject();
     116            // Don't throw exceptions here, since we may just point at the styling
     117            if ("visible".equalsIgnoreCase(layoutObject.getString("visibility", "visible"))) {
     118                switch (type) {
     119                case FILL:
     120                    // area
     121                    this.paint = parsePaintFill(paintObject);
     122                    break;
     123                case LINE:
     124                    // way
     125                    this.paint = parsePaintLine(layoutObject, paintObject);
     126                    break;
     127                case CIRCLE:
     128                    // point
     129                    this.paint = parsePaintCircle(paintObject);
     130                    break;
     131                case SYMBOL:
     132                    // point
     133                    this.paint = parsePaintSymbol(layoutObject, paintObject);
     134                    break;
     135                case BACKGROUND:
     136                    // canvas only
     137                    this.paint = parsePaintBackground(paintObject);
     138                    break;
     139                default:
     140                    this.paint = EMPTY_STRING;
     141                }
     142            } else {
     143                this.paint = EMPTY_STRING;
     144            }
     145        } else {
     146            this.paint = EMPTY_STRING;
     147        }
     148        this.sourceLayer = layerInfo.getString("source-layer", null);
     149    }
     150
     151    /**
     152     * Get the filter for this layer
     153     * @return The filter
     154     */
     155    public Expression getFilter() {
     156        return this.filter;
     157    }
     158
     159    /**
     160     * Get the unique id for this layer
     161     * @return The unique id
     162     */
     163    public String getId() {
     164        return this.id;
     165    }
     166
     167    /**
     168     * Get the type of this layer
     169     * @return The layer type
     170     */
     171    public Type getType() {
     172        return this.type;
     173    }
     174
     175    private static String parsePaintLine(final JsonObject layoutObject, final JsonObject paintObject) {
     176        final StringBuilder sb = new StringBuilder(36);
     177        // line-blur, default 0 (px)
     178        // line-color, default #000000, disabled by line-pattern
     179        final String color = paintObject.getString("line-color", "#000000");
     180        sb.append("color:").append(color).append(SEMI_COLON);
     181        // line-opacity, default 1 (0-1)
     182        final JsonNumber opacity = paintObject.getJsonNumber("line-opacity");
     183        if (opacity != null) {
     184            sb.append("opacity:").append(opacity.numberValue().doubleValue()).append(SEMI_COLON);
     185        }
     186        // line-cap, default butt (butt|round|square)
     187        final String cap = layoutObject.getString("line-cap", "butt");
     188        sb.append("linecap:");
     189        switch (cap) {
     190        case "round":
     191        case "square":
     192            sb.append(cap);
     193            break;
     194        case "butt":
     195        default:
     196            sb.append("none");
     197        }
     198
     199        sb.append(SEMI_COLON);
     200        // line-dasharray, array of number >= 0, units in line widths, disabled by line-pattern
     201        if (paintObject.containsKey("line-dasharray")) {
     202            final JsonArray dashArray = paintObject.getJsonArray("line-dasharray");
     203            sb.append("dashes:");
     204            sb.append(dashArray.stream().filter(JsonNumber.class::isInstance).map(JsonNumber.class::cast)
     205              .map(JsonNumber::toString).collect(Collectors.joining(",")));
     206            sb.append(SEMI_COLON);
     207        }
     208        // line-gap-width
     209        // line-gradient
     210        // line-join
     211        // line-miter-limit
     212        // line-offset
     213        // line-pattern TODO this first, since it disables stuff
     214        // line-round-limit
     215        // line-sort-key
     216        // line-translate
     217        // line-translate-anchor
     218        // line-width
     219        final JsonNumber width = paintObject.getJsonNumber("line-width");
     220        sb.append("width:").append(width == null ? 1 : width.toString()).append(SEMI_COLON);
     221        return sb.toString();
     222    }
     223
     224    private static String parsePaintCircle(final JsonObject paintObject) {
     225        final StringBuilder sb = new StringBuilder(150).append("symbol-shape:circle;")
     226          // circle-blur
     227          // circle-color
     228          .append("symbol-fill-color:").append(paintObject.getString("circle-color", "#000000")).append(SEMI_COLON);
     229        // circle-opacity
     230        final JsonNumber fillOpacity = paintObject.getJsonNumber("circle-opacity");
     231        sb.append("symbol-fill-opacity:").append(fillOpacity != null ? fillOpacity.numberValue().toString() : "1").append(SEMI_COLON);
     232        // circle-pitch-alignment // not 3D
     233        // circle-pitch-scale // not 3D
     234        // circle-radius
     235        final JsonNumber radius = paintObject.getJsonNumber("circle-radius");
     236        sb.append("symbol-size:").append(radius != null ? (2 * radius.numberValue().doubleValue()) : "10").append(SEMI_COLON)
     237          // circle-sort-key
     238          // circle-stroke-color
     239          .append("symbol-stroke-color:").append(paintObject.getString("circle-stroke-color", "#000000")).append(SEMI_COLON);
     240        // circle-stroke-opacity
     241        final JsonNumber strokeOpacity = paintObject.getJsonNumber("circle-stroke-opacity");
     242        sb.append("symbol-stroke-opacity:").append(strokeOpacity != null ? strokeOpacity.numberValue().toString() : "1").append(SEMI_COLON);
     243        // circle-stroke-width
     244        final JsonNumber strokeWidth = paintObject.getJsonNumber("circle-stroke-width");
     245        sb.append("symbol-stroke-width:").append(strokeWidth != null ? strokeWidth.numberValue().toString() : "0").append(SEMI_COLON);
     246        // circle-translate
     247        // circle-translate-anchor
     248        return sb.toString();
     249    }
     250
     251    private String parsePaintSymbol(
     252      final JsonObject layoutObject,
     253      final JsonObject paintObject) {
     254        final StringBuilder sb = new StringBuilder();
     255        // icon-allow-overlap
     256        // icon-anchor
     257        // icon-color
     258        // icon-halo-blur
     259        // icon-halo-color
     260        // icon-halo-width
     261        // icon-ignore-placement
     262        // icon-image
     263        boolean iconImage = false;
     264        if (layoutObject.containsKey("icon-image")) {
     265            sb.append("icon-image:concat(");
     266            if (this.styleId != null && !this.styleId.trim().isEmpty()) {
     267                sb.append('"').append(this.styleId).append('/').append("\",");
     268            }
     269            Matcher matcher = CURLY_BRACES.matcher(layoutObject.getString("icon-image"));
     270            StringBuffer stringBuffer = new StringBuffer();
     271            int previousMatch;
     272            if (matcher.lookingAt()) {
     273                matcher.appendReplacement(stringBuffer, "tag(\"$2\"),\"");
     274                previousMatch = matcher.end();
     275            } else {
     276                previousMatch = 0;
     277                stringBuffer.append('"');
     278            }
     279            while (matcher.find()) {
     280                if (matcher.start() == previousMatch) {
     281                    matcher.appendReplacement(stringBuffer, ",tag(\"$2\")");
     282                } else {
     283                    matcher.appendReplacement(stringBuffer, "\",tag(\"$2\"),\"");
     284                }
     285                previousMatch = matcher.end();
     286            }
     287            if (matcher.hitEnd() && stringBuffer.toString().endsWith(",\"")) {
     288                stringBuffer.delete(stringBuffer.length() - ",\"".length(), stringBuffer.length());
     289            } else if (!matcher.hitEnd()) {
     290                stringBuffer.append('"');
     291            }
     292            StringBuffer tail = new StringBuffer();
     293            matcher.appendTail(tail);
     294            if (tail.length() > 0) {
     295                String current = stringBuffer.toString();
     296                if (!"\"".equals(current) && !current.endsWith(",\"")) {
     297                    stringBuffer.append(",\"");
     298                }
     299                stringBuffer.append(tail);
     300                stringBuffer.append('"');
     301            }
     302
     303            sb.append(stringBuffer).append(')').append(SEMI_COLON);
     304            iconImage = true;
     305        }
     306        // icon-keep-upright
     307        // icon-offset
     308        if (iconImage && layoutObject.containsKey("icon-offset")) {
     309            // default [0, 0], right,down == positive, left,up == negative
     310            final List<JsonNumber> offset = layoutObject.getJsonArray("icon-offset").getValuesAs(JsonNumber.class);
     311            // Assume that the offset must be size 2. Probably not necessary, but docs aren't necessary clear.
     312            if (offset.size() == 2) {
     313                sb.append("icon-offset-x:").append(offset.get(0).doubleValue()).append(SEMI_COLON)
     314                  .append("icon-offset-y:").append(offset.get(1).doubleValue()).append(SEMI_COLON);
     315            }
     316        }
     317        // icon-opacity
     318        if (iconImage && paintObject.containsKey("icon-opacity")) {
     319            final double opacity = paintObject.getJsonNumber("icon-opacity").doubleValue();
     320            sb.append("icon-opacity:").append(opacity).append(SEMI_COLON);
     321        }
     322        // icon-optional
     323        // icon-padding
     324        // icon-pitch-alignment
     325        // icon-rotate
     326        if (iconImage && layoutObject.containsKey("icon-rotate")) {
     327            final double rotation = layoutObject.getJsonNumber("icon-rotate").doubleValue();
     328            sb.append("icon-rotation:").append(rotation).append(SEMI_COLON);
     329        }
     330        // icon-rotation-alignment
     331        // icon-size
     332        // icon-text-fit
     333        // icon-text-fit-padding
     334        // icon-translate
     335        // icon-translate-anchor
     336        // symbol-avoid-edges
     337        // symbol-placement
     338        // symbol-sort-key
     339        // symbol-spacing
     340        // symbol-z-order
     341        // text-allow-overlap
     342        // text-anchor
     343        // text-color
     344        if (paintObject.containsKey("text-color")) {
     345            sb.append("text-color:").append(paintObject.getString("text-color")).append(SEMI_COLON);
     346        }
     347        // text-field
     348        if (layoutObject.containsKey("text-field")) {
     349            sb.append("text:")
     350              .append(layoutObject.getString("text-field").replace("}", EMPTY_STRING).replace("{", EMPTY_STRING))
     351              .append(SEMI_COLON);
     352        }
     353        // text-font
     354        if (layoutObject.containsKey("text-font")) {
     355            List<String> fonts = layoutObject.getJsonArray("text-font").stream().filter(JsonString.class::isInstance)
     356              .map(JsonString.class::cast).map(JsonString::getString).collect(Collectors.toList());
     357            Font[] systemFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
     358            for (String fontString : fonts) {
     359                Collection<Font> fontMatches = Stream.of(systemFonts)
     360                  .filter(font -> Arrays.asList(font.getName(), font.getFontName(), font.getFamily(), font.getPSName()).contains(fontString))
     361                  .collect(Collectors.toList());
     362                if (!fontMatches.isEmpty()) {
     363                    final Font setFont = fontMatches.stream().filter(font -> font.getName().equals(fontString)).findAny()
     364                      .orElseGet(() -> fontMatches.stream().filter(font -> font.getFontName().equals(fontString)).findAny()
     365                        .orElseGet(() -> fontMatches.stream().filter(font -> font.getPSName().equals(fontString)).findAny()
     366                        .orElseGet(() -> fontMatches.stream().filter(font -> font.getFamily().equals(fontString)).findAny().orElse(null))));
     367                    if (setFont != null) {
     368                        sb.append("font-family:\"").append(setFont.getFamily()).append('"').append(SEMI_COLON);
     369                        sb.append("font-weight:").append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON);
     370                        sb.append("font-style:").append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON);
     371                        break;
     372                    }
     373                }
     374            }
     375        }
     376        // text-halo-blur
     377        // text-halo-color
     378        if (paintObject.containsKey("text-halo-color")) {
     379            sb.append("text-halo-color:").append(paintObject.getString("text-halo-color")).append(SEMI_COLON);
     380        }
     381        // text-halo-width
     382        if (paintObject.containsKey("text-halo-width")) {
     383            sb.append("text-halo-radius:").append(paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON);
     384        }
     385        // text-ignore-placement
     386        // text-justify
     387        // text-keep-upright
     388        // text-letter-spacing
     389        // text-line-height
     390        // text-max-angle
     391        // text-max-width
     392        // text-offset
     393        // text-opacity
     394        if (paintObject.containsKey("text-opacity")) {
     395            sb.append("text-opacity:").append(paintObject.getJsonNumber("text-opacity").doubleValue()).append(SEMI_COLON);
     396        }
     397        // text-optional
     398        // text-padding
     399        // text-pitch-alignment
     400        // text-radial-offset
     401        // text-rotate
     402        // text-rotation-alignment
     403        // text-size
     404        final JsonNumber textSize = layoutObject.getJsonNumber("text-size");
     405        sb.append("font-size:").append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON);
     406        // text-transform
     407        // text-translate
     408        // text-translate-anchor
     409        // text-variable-anchor
     410        // text-writing-mode
     411        return sb.toString();
     412    }
     413
     414    private static String parsePaintBackground(final JsonObject paintObject) {
     415        final StringBuilder sb = new StringBuilder(20);
     416        // background-color
     417        final String bgColor = paintObject.getString("background-color", null);
     418        if (bgColor != null) {
     419            sb.append("fill-color:").append(bgColor).append(SEMI_COLON);
     420        }
     421        // background-opacity
     422        // background-pattern
     423        return sb.toString();
     424    }
     425
     426    private static String parsePaintFill(final JsonObject paintObject) {
     427        StringBuilder sb = new StringBuilder(50)
     428          // fill-antialias
     429          // fill-color
     430          .append("fill-color:").append(paintObject.getString("fill-color", "#000000")).append(SEMI_COLON);
     431        // fill-opacity
     432        final JsonNumber opacity = paintObject.getJsonNumber("fill-opacity");
     433        sb.append("fill-opacity:").append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON)
     434          // fill-outline-color
     435          .append("color:").append(paintObject.getString("fill-outline-color",
     436          paintObject.getString("fill-color", "#000000"))).append(SEMI_COLON);
     437        // fill-pattern
     438        // fill-sort-key
     439        // fill-translate
     440        // fill-translate-anchor
     441        return sb.toString();
     442    }
     443
     444    /**
     445     * Converts this layer object to a mapcss entry string (to be parsed later)
     446     * @return The mapcss entry (string form)
     447     */
     448    @Override
     449    public String toString() {
     450        if (this.filter.toString().isEmpty() && this.paint.isEmpty()) {
     451            return EMPTY_STRING;
     452        } else if (this.type == Type.BACKGROUND) {
     453            // AFAIK, paint has no zoom levels, and doesn't accept a layer
     454            return "canvas{" + this.paint + "}";
     455        }
     456
     457        final String zoomSelector;
     458        if (this.minZoom == this.maxZoom) {
     459            zoomSelector = "|z" + this.minZoom;
     460        } else if (this.minZoom > Integer.MIN_VALUE && this.maxZoom == Integer.MAX_VALUE) {
     461            zoomSelector = "|z" + this.minZoom + "-";
     462        } else if (this.minZoom == Integer.MIN_VALUE && this.maxZoom < Integer.MAX_VALUE) {
     463            zoomSelector = "|z-" + this.maxZoom;
     464        } else if (this.minZoom > Integer.MIN_VALUE) {
     465            zoomSelector = MessageFormat.format("|z{0}-{1}", this.minZoom, this.maxZoom);
     466        } else {
     467            zoomSelector = EMPTY_STRING;
     468        }
     469        final String commonData = zoomSelector + this.filter.toString() + "::" + this.id + "{" + this.paint + "}";
     470
     471        if (this.type == Type.CIRCLE || this.type == Type.SYMBOL) {
     472            return "node" + commonData;
     473        } else if (this.type == Type.FILL) {
     474            return "area" + commonData;
     475        } else if (this.type == Type.LINE) {
     476            return "way" + commonData;
     477        }
     478        return super.toString();
     479    }
     480
     481    /**
     482     * Get the source that this applies to
     483     * @return The source name
     484     */
     485    public String getSource() {
     486        return this.source;
     487    }
     488
     489    /**
     490     * Get the layer that this applies to
     491     * @return The layer name
     492     */
     493    public String getSourceLayer() {
     494        return this.sourceLayer;
     495    }
     496
     497    @Override
     498    public boolean equals(Object other) {
     499        if (other != null && this.getClass() == other.getClass()) {
     500            Layers o = (Layers) other;
     501            return this.type == o.type
     502              && this.minZoom == o.minZoom
     503              && this.maxZoom == o.maxZoom
     504              && Objects.equals(this.id, o.id)
     505              && Objects.equals(this.styleId, o.styleId)
     506              && Objects.equals(this.sourceLayer, o.sourceLayer)
     507              && Objects.equals(this.source, o.source)
     508              && Objects.equals(this.filter, o.filter)
     509              && Objects.equals(this.paint, o.paint);
     510        }
     511        return false;
     512    }
     513
     514    @Override
     515    public int hashCode() {
     516        return Objects.hash(this.type, this.minZoom, this.maxZoom, this.id, this.styleId, this.sourceLayer, this.source,
     517          this.filter, this.paint);
     518    }
     519}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
    new file mode 100644
    index 000000000..746913042
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Image;
     7import java.awt.image.BufferedImage;
     8import java.io.BufferedReader;
     9import java.io.File;
     10import java.io.IOException;
     11import java.io.InputStream;
     12import java.io.OutputStream;
     13import java.nio.charset.StandardCharsets;
     14import java.nio.file.Files;
     15import java.util.Collections;
     16import java.util.LinkedHashMap;
     17import java.util.List;
     18import java.util.Map;
     19import java.util.Objects;
     20import java.util.Optional;
     21import java.util.concurrent.ConcurrentHashMap;
     22import java.util.stream.Collectors;
     23
     24import javax.imageio.ImageIO;
     25import javax.json.Json;
     26import javax.json.JsonArray;
     27import javax.json.JsonObject;
     28import javax.json.JsonReader;
     29import javax.json.JsonStructure;
     30import javax.json.JsonValue;
     31
     32import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
     33import org.openstreetmap.josm.gui.MainApplication;
     34import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     35import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
     36import org.openstreetmap.josm.io.CachedFile;
     37import org.openstreetmap.josm.spi.preferences.Config;
     38import org.openstreetmap.josm.tools.Logging;
     39
     40/**
     41 * Create a mapping for a Mapbox Vector Style
     42 *
     43 * @author Taylor Smock
     44 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a>
     45 * @since xxx
     46 */
     47public class MapBoxVectorStyle {
     48
     49    private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
     50
     51    /**
     52     * Get a MapBoxVector style for a URL
     53     * @param url The url to get
     54     * @return The MapBox Vector Style. May be {@code null} if there was an error.
     55     */
     56    public static MapBoxVectorStyle getMapBoxVectorStyle(String url) {
     57        return STYLE_MAPPING.computeIfAbsent(url, key -> {
     58            try (CachedFile style = new CachedFile(url);
     59                    BufferedReader reader = style.getContentReader();
     60                    JsonReader jsonReader = Json.createReader(reader)) {
     61                JsonStructure structure = jsonReader.read();
     62                return new MapBoxVectorStyle(structure.asJsonObject());
     63            } catch (IOException e) {
     64                Logging.error(e);
     65            }
     66            // Documentation indicates that this will <i>not</i> be entered into the map, which means that this will be
     67            // retried if something goes wrong.
     68            return null;
     69        });
     70    }
     71
     72    /** The version for the style specification */
     73    private final int version;
     74    /** The optional name for the vector style */
     75    private final String name;
     76    /** The optional URL for sprites. This mush be absolute (so it must contain the scheme, authority, and path). */
     77    private final String spriteUrl;
     78    /** The optional URL for glyphs. This may have replaceable values in it. */
     79    private final String glyphUrl;
     80    /** The required collection of sources with a list of layers that are applicable for that source*/
     81    private final Map<Source, ElemStyles> sources;
     82
     83    /**
     84     * Create a new MapBoxVector style. You should prefer {@link #getMapBoxVectorStyle(String)}
     85     * for deduplication purposes.
     86     *
     87     * @param jsonObject The object to create the style from
     88     * @see #getMapBoxVectorStyle(String)
     89     */
     90    public MapBoxVectorStyle(JsonObject jsonObject) {
     91        // There should be a version specifier. We currently only support version 8.
     92        // This can throw an NPE when there is no version number.
     93        this.version = jsonObject.getInt("version");
     94        if (this.version == 8) {
     95            this.name = jsonObject.getString("name", null);
     96            String id = jsonObject.getString("id", this.name);
     97            this.spriteUrl = jsonObject.getString("sprite", null);
     98            this.glyphUrl = jsonObject.getString("glyphs", null);
     99            final List<Source> sourceList;
     100            if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) {
     101                final JsonObject sourceObj = jsonObject.getJsonObject("sources");
     102                sourceList = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT)
     103                  .map(entry -> new Source(entry.getKey(), entry.getValue().asJsonObject())).collect(Collectors.toList());
     104            } else {
     105                sourceList = Collections.emptyList();
     106            }
     107            final List<Layers> layers;
     108            if (jsonObject.containsKey("layers") && jsonObject.get("layers").getValueType() == JsonValue.ValueType.ARRAY) {
     109                JsonArray lArray = jsonObject.getJsonArray("layers");
     110                layers = lArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast).map(obj -> new Layers(id, obj))
     111                  .collect(Collectors.toList());
     112            } else {
     113                layers = Collections.emptyList();
     114            }
     115            final Map<Optional<Source>, List<Layers>> sourceLayer = layers.stream().collect(
     116              Collectors.groupingBy(layer -> sourceList.stream().filter(source -> source.getName().equals(layer.getSource()))
     117                .findFirst(), LinkedHashMap::new, Collectors.toList()));
     118            // Abuse HashMap null (null == default)
     119            this.sources = new LinkedHashMap<>();
     120            for (Map.Entry<Optional<Source>, List<Layers>> entry : sourceLayer.entrySet()) {
     121                final Source source = entry.getKey().orElse(null);
     122                final String data = entry.getValue().stream().map(Layers::toString).collect(Collectors.joining());
     123                final String metaData = "meta{title:" + (source == null ? "Generated Style" :
     124                  source.getName()) + ";version:\"autogenerated\";description:\"auto generated style\";}";
     125
     126                // This is the default canvas
     127                final String canvas = "canvas{default-points:false;default-lines:false;}";
     128                final MapCSSStyleSource style = new MapCSSStyleSource(metaData + canvas + data);
     129                // Save to directory
     130                MainApplication.worker.execute(() -> this.save((source == null ? data.hashCode() : source.getName()) + ".mapcss", style));
     131                this.sources.put(source, new ElemStyles(Collections.singleton(style)));
     132            }
     133            if (this.spriteUrl != null && !this.spriteUrl.trim().isEmpty()) {
     134                MainApplication.worker.execute(this::fetchSprites);
     135            }
     136        } else {
     137            throw new IllegalArgumentException(tr("Vector Tile Style Version not understood: version {0} (json: {1})",
     138              this.version, jsonObject));
     139        }
     140    }
     141
     142    /**
     143     * Fetch sprites. Please note that this is (literally) only png. Unfortunately.
     144     * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/</a>
     145     */
     146    private void fetchSprites() {
     147        // HiDPI images first -- if this succeeds, don't bother with the lower resolution (JOSM has no method to switch)
     148        try (CachedFile spriteJson = new CachedFile(this.spriteUrl + "@2x.json");
     149          CachedFile spritePng = new CachedFile(this.spriteUrl + "@2x.png")) {
     150            if (parseSprites(spriteJson, spritePng)) {
     151                return;
     152            }
     153        }
     154        try (CachedFile spriteJson = new CachedFile(this.spriteUrl + ".json");
     155        CachedFile spritePng = new CachedFile(this.spriteUrl + ".png")) {
     156            parseSprites(spriteJson, spritePng);
     157        }
     158    }
     159
     160    private boolean parseSprites(CachedFile spriteJson, CachedFile spritePng) {
     161        /* JSON looks like this:
     162         * { "image-name": {"width": width, "height": height, "x": x, "y": y, "pixelRatio": 1 }}
     163         * width/height are the dimensions of the image
     164         * x -- distance right from top left
     165         * y -- distance down from top left
     166         * pixelRatio -- this <i>appears</i> to be from the "@2x" (default 1)
     167         * content -- [left, top corner, right, bottom corner]
     168         * stretchX -- [[from, to], [from, to], ...]
     169         * stretchY -- [[from, to], [from, to], ...]
     170         */
     171        final JsonObject spriteObject;
     172        final BufferedImage spritePngImage;
     173        try (BufferedReader spriteJsonBufferedReader = spriteJson.getContentReader();
     174          JsonReader spriteJsonReader = Json.createReader(spriteJsonBufferedReader);
     175          InputStream spritePngBufferedReader = spritePng.getInputStream()
     176        ) {
     177            spriteObject = spriteJsonReader.read().asJsonObject();
     178            spritePngImage = ImageIO.read(spritePngBufferedReader);
     179        } catch (IOException e) {
     180            Logging.error(e);
     181            return false;
     182        }
     183        for (Map.Entry<String, JsonValue> entry : spriteObject.entrySet()) {
     184            final JsonObject info = entry.getValue().asJsonObject();
     185            int width = info.getInt("width");
     186            int height = info.getInt("height");
     187            int x = info.getInt("x");
     188            int y = info.getInt("y");
     189            save(entry.getKey() + ".png", spritePngImage.getSubimage(x, y, width, height));
     190        }
     191        return true;
     192    }
     193
     194    private void save(String name, Object object) {
     195        final File cache;
     196        if (object instanceof Image) {
     197            // Images have a specific location where they are looked for
     198            cache = new File(Config.getDirs().getUserDataDirectory(true), "images");
     199        } else {
     200            cache = JosmBaseDirectories.getInstance().getCacheDirectory(true);
     201        }
     202        final File location = new File(cache, this.name != null ? this.name : Integer.toString(this.hashCode()));
     203        if ((!location.exists() && !location.mkdirs()) || (location.exists() && !location.isDirectory())) {
     204            // Don't try to save if the file exists and is not a directory or we couldn't create it
     205            return;
     206        }
     207        final File toSave = new File(location, name);
     208        try (OutputStream fileOutputStream = Files.newOutputStream(toSave.toPath())) {
     209            if (object instanceof String) {
     210                fileOutputStream.write(((String) object).getBytes(StandardCharsets.UTF_8));
     211            } else if (object instanceof MapCSSStyleSource) {
     212                MapCSSStyleSource source = (MapCSSStyleSource) object;
     213                try (InputStream inputStream = source.getSourceInputStream()) {
     214                    int byteVal = inputStream.read();
     215                    do {
     216                        fileOutputStream.write(byteVal);
     217                        byteVal = inputStream.read();
     218                    } while (byteVal > -1);
     219                    source.url = "file:/" + toSave.getAbsolutePath().replace('\\', '/');
     220                    if (source.isLoaded()) {
     221                        source.loadStyleSource();
     222                    }
     223                }
     224            } else if (object instanceof BufferedImage) {
     225                // This directory is checked first when getting images
     226                ImageIO.write((BufferedImage) object, "png", toSave);
     227            }
     228        } catch (IOException e) {
     229            Logging.info(e);
     230        }
     231    }
     232
     233    /**
     234     * Get the generated layer->style mapping
     235     * @return The mapping (use to enable/disable a paint style)
     236     */
     237    public Map<Source, ElemStyles> getSources() {
     238        return this.sources;
     239    }
     240
     241    /**
     242     * Get the sprite url for the style
     243     * @return The base sprite url
     244     */
     245    public String getSpriteUrl() {
     246        return this.spriteUrl;
     247    }
     248
     249    @Override
     250    public boolean equals(Object other) {
     251        if (other != null && other.getClass() == this.getClass()) {
     252            MapBoxVectorStyle o = (MapBoxVectorStyle) other;
     253            return this.version == o.version
     254              && Objects.equals(this.name, o.name)
     255              && Objects.equals(this.glyphUrl, o.glyphUrl)
     256              && Objects.equals(this.spriteUrl, o.spriteUrl)
     257              && Objects.equals(this.sources, o.sources);
     258        }
     259        return false;
     260    }
     261
     262    @Override
     263    public int hashCode() {
     264        return Objects.hash(this.name, this.version, this.glyphUrl, this.spriteUrl, this.sources);
     265    }
     266}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
    new file mode 100644
    index 000000000..e8583b940
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4/**
     5 * The scheme used for tiles
     6 */
     7public enum Scheme {
     8    /** Standard slippy map scheme */
     9    XYZ,
     10    /** OSGeo specification scheme */
     11    TMS
     12}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
    new file mode 100644
    index 000000000..dc7c62d62
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import java.text.MessageFormat;
     5import java.util.ArrayList;
     6import java.util.Arrays;
     7import java.util.Collection;
     8import java.util.Collections;
     9import java.util.List;
     10import java.util.Locale;
     11import java.util.Objects;
     12import java.util.function.IntFunction;
     13
     14import javax.json.JsonArray;
     15import javax.json.JsonObject;
     16import javax.json.JsonString;
     17import javax.json.JsonValue;
     18
     19import org.openstreetmap.josm.data.Bounds;
     20
     21/**
     22 * A source from a MapBox Vector Style
     23 *
     24 * @author Taylor Smock
     25 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a>
     26 * @since xxx
     27 */
     28public class Source {
     29    /**
     30     * A common function for zoom constraints
     31     */
     32    private static class ZoomBoundFunction implements IntFunction<Integer> {
     33        private final int min;
     34        private final int max;
     35        /**
     36         * Create a new bound for zooms
     37         * @param min The min zoom
     38         * @param max The max zoom
     39         */
     40        ZoomBoundFunction(int min, int max) {
     41            this.min = min;
     42            this.max = max;
     43        }
     44
     45        @Override public Integer apply(int value) {
     46            return Math.max(min, Math.min(value, max));
     47        }
     48    }
     49
     50    /**
     51     * WMS servers should contain a "{bbox-epsg-3857}" parameter for the bbox
     52     */
     53    private static final String WMS_BBOX = "bbox-epsg-3857";
     54
     55    private static final String[] NO_URLS = new String[0];
     56
     57    /**
     58     * Constrain the min/max zooms to be between 0 and 30, as per tilejson spec
     59     */
     60    private static final IntFunction<Integer> ZOOM_BOUND_FUNCTION = new ZoomBoundFunction(0, 30);
     61
     62    /* Common items */
     63    /**
     64     * The name of the source
     65     */
     66    private final String name;
     67    /**
     68     * The type of the source
     69     */
     70    private final SourceType sourceType;
     71
     72    /* Common tiled data */
     73    /**
     74     * The minimum zoom supported
     75     */
     76    private final int minZoom;
     77    /**
     78     * The maximum zoom supported
     79     */
     80    private final int maxZoom;
     81    /**
     82     * The tile urls. These usually have replaceable fields.
     83     */
     84    private final String[] tileUrls;
     85
     86    /* Vector and raster data */
     87    /**
     88     * The attribution to display for the user
     89     */
     90    private final String attribution;
     91    /**
     92     * The bounds of the data. We should not request data outside of the bounds
     93     */
     94    private final Bounds bounds;
     95    /**
     96     * The property to use as a feature id. Can be parameterized
     97     */
     98    private final String promoteId;
     99    /**
     100     * The tile scheme
     101     */
     102    private final Scheme scheme;
     103    /**
     104     * {@code true} if the tiles should not be cached
     105     */
     106    private final boolean volatileCache;
     107
     108    /* Raster data */
     109    /**
     110     * The tile size
     111     */
     112    private final int tileSize;
     113
     114    /**
     115     * Create a new Source object
     116     *
     117     * @param name The name of the source object
     118     * @param data The data to set the source information with
     119     */
     120    public Source(final String name, final JsonObject data) {
     121        Objects.requireNonNull(name, "Name cannot be null");
     122        Objects.requireNonNull(data, "Data cannot be null");
     123        this.name = name;
     124        // "type" is required (so throw an NPE if it doesn't exist)
     125        final String type = data.getString("type");
     126        this.sourceType = SourceType.valueOf(type.replace("-", "_").toUpperCase(Locale.ROOT));
     127        // This can also contain SourceType.RASTER_DEM (only needs encoding)
     128        if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) {
     129            if (data.containsKey("url")) {
     130                // TODO implement https://github.com/mapbox/tilejson-spec
     131                throw new UnsupportedOperationException();
     132            } else {
     133                this.minZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("minzoom", 0));
     134                this.maxZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("maxzoom", 22));
     135                this.attribution = data.getString("attribution", null);
     136                if (data.containsKey("bounds") && data.get("bounds").getValueType() == JsonValue.ValueType.ARRAY) {
     137                    final JsonArray bJsonArray = data.getJsonArray("bounds");
     138                    if (bJsonArray.size() != 4) {
     139                        throw new IllegalArgumentException(MessageFormat.format("bounds must have four values, but has {0}", bJsonArray.size()));
     140                    }
     141                    final double[] bArray = new double[bJsonArray.size()];
     142                    for (int i = 0; i < bJsonArray.size(); i++) {
     143                        bArray[i] = bJsonArray.getJsonNumber(i).doubleValue();
     144                    }
     145                    // The order in the response is
     146                    // [south-west longitude, south-west latitude, north-east longitude, north-east latitude]
     147                    this.bounds = new Bounds(bArray[1], bArray[0], bArray[3], bArray[2]);
     148                } else {
     149                    // Don't use a static instance for bounds, as it is not a immutable class
     150                    this.bounds = new Bounds(-85.051129, -180, 85.051129, 180);
     151                }
     152                this.promoteId = data.getString("promoteId", null);
     153                this.scheme = Scheme.valueOf(data.getString("scheme", "xyz").toUpperCase(Locale.ROOT));
     154                if (data.containsKey("tiles") && data.get("tiles").getValueType() == JsonValue.ValueType.ARRAY) {
     155                    this.tileUrls = data.getJsonArray("tiles").stream().filter(JsonString.class::isInstance)
     156                      .map(JsonString.class::cast).map(JsonString::getString)
     157                      // Replace bbox-epsg-3857 with bbox (already encased with {})
     158                      .map(url -> url.replace(WMS_BBOX, "bbox")).toArray(String[]::new);
     159                } else {
     160                    this.tileUrls = NO_URLS;
     161                }
     162                this.volatileCache = data.getBoolean("volatile", false);
     163                this.tileSize = data.getInt("tileSize", 512);
     164            }
     165        } else {
     166            throw new UnsupportedOperationException();
     167        }
     168    }
     169
     170    /**
     171     * Get the bounds for this source
     172     * @return The bounds where this source can be used
     173     */
     174    public Bounds getBounds() {
     175        return this.bounds;
     176    }
     177
     178    /**
     179     * Get the source name
     180     * @return the name
     181     */
     182    public String getName() {
     183        return name;
     184    }
     185
     186    /**
     187     * Get the URLs that can be used to get vector data
     188     *
     189     * @return The urls
     190     */
     191    public List<String> getUrls() {
     192        return Collections.unmodifiableList(Arrays.asList(this.tileUrls));
     193    }
     194
     195    /**
     196     * Get the minimum zoom
     197     *
     198     * @return The min zoom (default {@code 0})
     199     */
     200    public int getMinZoom() {
     201        return this.minZoom;
     202    }
     203
     204    /**
     205     * Get the max zoom
     206     *
     207     * @return The max zoom (default {@code 22})
     208     */
     209    public int getMaxZoom() {
     210        return this.maxZoom;
     211    }
     212
     213    /**
     214     * Get the attribution for this source
     215     *
     216     * @return The attribution text. May be {@code null}.
     217     */
     218    public String getAttributionText() {
     219        return this.attribution;
     220    }
     221
     222    @Override
     223    public String toString() {
     224        Collection<String> parts = new ArrayList<>(1 + this.getUrls().size());
     225        parts.add(this.getName());
     226        parts.addAll(this.getUrls());
     227        return String.join(" ", parts);
     228    }
     229
     230    @Override
     231    public boolean equals(Object other) {
     232        if (other != null && this.getClass() == other.getClass()) {
     233            Source o = (Source) other;
     234            return Objects.equals(this.name, o.name)
     235              && this.sourceType == o.sourceType
     236              && this.minZoom == o.minZoom
     237              && this.maxZoom == o.maxZoom
     238              && Objects.equals(this.attribution, o.attribution)
     239              && Objects.equals(this.promoteId, o.promoteId)
     240              && this.scheme == o.scheme
     241              && this.volatileCache == o.volatileCache
     242              && this.tileSize == o.tileSize
     243              && Objects.equals(this.bounds, o.bounds)
     244              && Objects.deepEquals(this.tileUrls, o.tileUrls);
     245        }
     246        return false;
     247    }
     248
     249    @Override
     250    public int hashCode() {
     251        return Objects.hash(this.name, this.sourceType, this.minZoom, this.maxZoom, this.attribution, this.promoteId,
     252          this.scheme, this.volatileCache, this.tileSize, this.bounds, Arrays.hashCode(this.tileUrls));
     253    }
     254}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java

    diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
    new file mode 100644
    index 000000000..a086289d6
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4/**
     5 * The "source type" for the data (MapBox Vector Style specification)
     6 *
     7 * @author Taylor Smock
     8 * @since xxx
     9 */
     10public enum SourceType {
     11    VECTOR,
     12    RASTER,
     13    RASTER_DEM,
     14    GEOJSON,
     15    IMAGE,
     16    VIDEO
     17}
  • src/org/openstreetmap/josm/data/osm/IPrimitive.java

    diff --git src/org/openstreetmap/josm/data/osm/IPrimitive.java src/org/openstreetmap/josm/data/osm/IPrimitive.java
    index cdabcd1b6..34e2ca0be 100644
    public interface IPrimitive extends IQuadBucketType, Tagged, PrimitiveId, Stylab  
    391391        return getName();
    392392    }
    393393
     394    /**
     395     * Get an object to synchronize the style cache on. This <i>should</i> be a field that does not change during paint.
     396     * By default, it returns the current object, but should be overriden to avoid some performance issues.
     397     * @return A non-{@code null} object to synchronize on when painting
     398     */
     399    default Object getStyleCacheSyncObject() {
     400        return this;
     401    }
     402
    394403    /**
    395404     * Replies the display name of a primitive formatted by <code>formatter</code>
    396405     * @param formatter formatter to use
  • src/org/openstreetmap/josm/data/osm/IRelationMember.java

    diff --git src/org/openstreetmap/josm/data/osm/IRelationMember.java src/org/openstreetmap/josm/data/osm/IRelationMember.java
    index c2803e38d..69091056d 100644
    public interface IRelationMember<P extends IPrimitive> extends PrimitiveId {  
    6666     * @since 13766 (IRelationMember)
    6767     */
    6868    P getMember();
     69
     70    /**
     71     * Returns the relation member as a way.
     72     * @return Member as a way
     73     * @since xxx
     74     */
     75    default IWay<?> getWay() {
     76        return (IWay<?>) getMember();
     77    }
    6978}
  • new file src/org/openstreetmap/josm/data/osm/IWaySegment.java

    diff --git src/org/openstreetmap/josm/data/osm/IWaySegment.java src/org/openstreetmap/josm/data/osm/IWaySegment.java
    new file mode 100644
    index 000000000..0735935de
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm;
     3
     4import java.awt.geom.Line2D;
     5import java.lang.reflect.Constructor;
     6import java.lang.reflect.InvocationTargetException;
     7import java.util.Arrays;
     8import java.util.Objects;
     9
     10import org.openstreetmap.josm.tools.Logging;
     11
     12/**
     13 * A segment consisting of 2 consecutive nodes out of a way.
     14 * @author Taylor Smock
     15 * @param <N> The node type
     16 * @param <W> The way type
     17 * @since xxx
     18 */
     19public class IWaySegment<N extends INode, W extends IWay<N>> implements Comparable<IWaySegment<N, W>> {
     20
     21    /**
     22     * The way.
     23     */
     24    public final W way;
     25
     26    /**
     27     * The index of one of the 2 nodes in the way.  The other node has the
     28     * index <code>lowerIndex + 1</code>.
     29     */
     30    public final int lowerIndex;
     31
     32    /**
     33     * Constructs a new {@code IWaySegment}.
     34     * @param w The way
     35     * @param i The node lower index
     36     * @throws IllegalArgumentException in case of invalid index
     37     */
     38    public IWaySegment(W w, int i) {
     39        way = w;
     40        lowerIndex = i;
     41        if (i < 0 || i >= w.getNodesCount() - 1) {
     42            throw new IllegalArgumentException(toString());
     43        }
     44    }
     45
     46    /**
     47     * Returns the first node of the way segment.
     48     * @return the first node
     49     */
     50    public N getFirstNode() {
     51        return way.getNode(lowerIndex);
     52    }
     53
     54    /**
     55     * Returns the second (last) node of the way segment.
     56     * @return the second node
     57     */
     58    public N getSecondNode() {
     59        return way.getNode(lowerIndex + 1);
     60    }
     61
     62    /**
     63     * Determines and returns the way segment for the given way and node pair.
     64     * @param way way
     65     * @param first first node
     66     * @param second second node
     67     * @return way segment
     68     * @throws IllegalArgumentException if the node pair is not part of way
     69     */
     70    public static <N extends INode, W extends IWay<N>> IWaySegment<N, W> forNodePair(W way, N first, N second) {
     71        int endIndex = way.getNodesCount() - 1;
     72        while (endIndex > 0) {
     73            final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first);
     74            if (second.equals(way.getNode(indexOfFirst + 1))) {
     75                return new IWaySegment<>(way, indexOfFirst);
     76            }
     77            endIndex--;
     78        }
     79        throw new IllegalArgumentException("Node pair is not part of way!");
     80    }
     81
     82    /**
     83     * Returns this way segment as complete way.
     84     * @return the way segment as {@code Way}
     85     * @throws IllegalAccessException See {@link Constructor#newInstance}
     86     * @throws IllegalArgumentException See {@link Constructor#newInstance}
     87     * @throws InstantiationException See {@link Constructor#newInstance}
     88     * @throws InvocationTargetException See {@link Constructor#newInstance}
     89     * @throws NoSuchMethodException See {@link Class#getConstructor}
     90     */
     91    public W toWay()
     92      throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
     93        // If the number of nodes is 2, then don't bother creating a new way
     94        if (this.way.getNodes().size() == 2) {
     95            return this.way;
     96        }
     97        // Since the way determines the generic class, this.way.getClass() is always Class<W>, assuming
     98        // that way remains the defining element for the type, and remains final.
     99        @SuppressWarnings("unchecked")
     100        Class<W> clazz = (Class<W>) this.way.getClass();
     101        Constructor<W> constructor;
     102        W w;
     103        try {
     104            // Check for clone constructor
     105            constructor = clazz.getConstructor(clazz);
     106            w = constructor.newInstance(this.way);
     107        } catch (NoSuchMethodException e) {
     108            Logging.trace(e);
     109            constructor = clazz.getConstructor();
     110            w = constructor.newInstance();
     111        }
     112
     113        w.setNodes(Arrays.asList(getFirstNode(), getSecondNode()));
     114        return w;
     115    }
     116
     117    @Override
     118    public boolean equals(Object o) {
     119        if (this == o) return true;
     120        if (o == null || getClass() != o.getClass()) return false;
     121        IWaySegment<?, ?> that = (IWaySegment<?, ?>) o;
     122        return lowerIndex == that.lowerIndex &&
     123          Objects.equals(way, that.way);
     124    }
     125
     126    @Override
     127    public int hashCode() {
     128        return Objects.hash(way, lowerIndex);
     129    }
     130
     131    @Override
     132    public int compareTo(IWaySegment o) {
     133        final W thisWay;
     134        final IWay<?> otherWay;
     135        try {
     136            thisWay = toWay();
     137            otherWay = o == null ? null : o.toWay();
     138        } catch (ReflectiveOperationException e) {
     139            Logging.error(e);
     140            return -1;
     141        }
     142        return o == null ? -1 : (equals(o) ? 0 : thisWay.compareTo(otherWay));
     143    }
     144
     145    /**
     146     * Checks whether this segment crosses other segment
     147     *
     148     * @param s2 The other segment
     149     * @return true if both segments crosses
     150     */
     151    public boolean intersects(IWaySegment<?, ?> s2) {
     152        if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||
     153          getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))
     154            return false;
     155
     156        return Line2D.linesIntersect(
     157          getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),
     158          getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),
     159          s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),
     160          s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
     161    }
     162
     163    /**
     164     * Checks whether this segment and another way segment share the same points
     165     * @param s2 The other segment
     166     * @return true if other way segment is the same or reverse
     167     */
     168    public boolean isSimilar(IWaySegment<?, ?> s2) {
     169        return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
     170          || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));
     171    }
     172
     173    @Override
     174    public String toString() {
     175        return "IWaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
     176    }
     177}
  • src/org/openstreetmap/josm/data/osm/RelationMember.java

    diff --git src/org/openstreetmap/josm/data/osm/RelationMember.java src/org/openstreetmap/josm/data/osm/RelationMember.java
    index fc62c71f3..5add40403 100644
    public class RelationMember implements IRelationMember<OsmPrimitive> {  
    5757     * @return Member as way
    5858     * @since 1937
    5959     */
     60    @Override
    6061    public Way getWay() {
    6162        return (Way) member;
    6263    }
  • src/org/openstreetmap/josm/data/osm/WaySegment.java

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

    diff --git src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
    index 038374233..03ba0e5b2 100644
    import java.util.Objects;  
    3636import java.util.Optional;
    3737import java.util.concurrent.ForkJoinPool;
    3838import java.util.concurrent.TimeUnit;
     39import java.util.concurrent.locks.Lock;
    3940import java.util.function.BiConsumer;
    4041import java.util.function.Consumer;
    4142import java.util.function.Supplier;
    public class StyledMapRenderer extends AbstractMapRenderer {  
    16371638        RenderBenchmarkCollector benchmark = benchmarkFactory.get();
    16381639        BBox bbox = bounds.toBBox();
    16391640        getSettings(renderVirtualNodes);
    1640 
    16411641        try {
    1642             if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) {
     1642            Lock readLock = data.getReadLock();
     1643            if (readLock.tryLock(1, TimeUnit.SECONDS)) {
    16431644                try {
    16441645                    paintWithLock(data, renderVirtualNodes, benchmark, bbox);
    16451646                } finally {
    1646                     data.getReadLock().unlock();
     1647                    readLock.unlock();
    16471648                }
    16481649            } else {
    16491650                Logging.warn("Cannot paint layer {0}: It is locked.");
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

    diff --git src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
    new file mode 100644
    index 000000000..109f8915a
    - +  
     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 *
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public class ProtoBufPacked {
     14    private final byte[] bytes;
     15    private final Number[] numbers;
     16    private int location;
     17
     18    /**
     19     * Create a new ProtoBufPacked object
     20     *
     21     * @param bytes The packed bytes
     22     */
     23    public ProtoBufPacked(byte[] bytes) {
     24        this.location = 0;
     25        this.bytes = bytes;
     26        List<Number> numbersT = new ArrayList<>();
     27        while (this.location < bytes.length) {
     28            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
     29        }
     30
     31        this.numbers = new Number[numbersT.size()];
     32        for (int i = 0; i < numbersT.size(); i++) {
     33            this.numbers[i] = numbersT.get(i);
     34        }
     35    }
     36
     37    /**
     38     * Get the parsed number array
     39     *
     40     * @return The number array
     41     */
     42    public Number[] getArray() {
     43        return this.numbers;
     44    }
     45
     46    private byte[] nextVarInt() {
     47        List<Byte> byteList = new ArrayList<>();
     48        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
     49          == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     50            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     51            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     52        }
     53        // The last byte doesn't drop the most significant bit
     54        byteList.add(this.bytes[this.location++]);
     55        byte[] byteArray = new byte[byteList.size()];
     56        for (int i = 0; i < byteList.size(); i++) {
     57            byteArray[i] = byteList.get(i);
     58        }
     59
     60        return byteArray;
     61    }
     62}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

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

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

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

    diff --git src/org/openstreetmap/josm/data/vector/DataLayer.java src/org/openstreetmap/josm/data/vector/DataLayer.java
    new file mode 100644
    index 000000000..9052e5b1a
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4/**
     5 * An interface for objects that are part of a data layer
     6 * @param <T> The type used to identify a layer, typically a string
     7 */
     8public interface DataLayer<T> {
     9    /**
     10     * Get the layer
     11     * @return The layer
     12     */
     13    T getLayer();
     14
     15    /**
     16     * Set the layer
     17     * @param layer The layer to set
     18     * @return {@code true} if the layer was set -- some objects may never change layers.
     19     */
     20    default boolean setLayer(T layer) {
     21        return layer != null && layer.equals(getLayer());
     22    }
     23}
  • new file src/org/openstreetmap/josm/data/vector/DataStore.java

    diff --git src/org/openstreetmap/josm/data/vector/DataStore.java src/org/openstreetmap/josm/data/vector/DataStore.java
    new file mode 100644
    index 000000000..9de044f62
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.util.Collection;
     5import java.util.Collections;
     6import java.util.HashMap;
     7import java.util.HashSet;
     8import java.util.LinkedList;
     9import java.util.Map;
     10import java.util.Set;
     11import java.util.concurrent.locks.ReentrantReadWriteLock;
     12
     13import org.openstreetmap.gui.jmapviewer.Tile;
     14import org.openstreetmap.josm.data.DataSource;
     15import org.openstreetmap.josm.data.osm.DataSet;
     16import org.openstreetmap.josm.data.osm.INode;
     17import org.openstreetmap.josm.data.osm.IPrimitive;
     18import org.openstreetmap.josm.data.osm.IRelation;
     19import org.openstreetmap.josm.data.osm.IWay;
     20import org.openstreetmap.josm.data.osm.PrimitiveId;
     21import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore;
     22import org.openstreetmap.josm.data.osm.Storage;
     23
     24/**
     25 * A class that stores data (essentially a simple {@link DataSet})
     26 * @author Taylor Smock
     27 * @since xxx
     28 */
     29class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> {
     30    /**
     31     * This literally only exists to make {@link QuadBucketPrimitiveStore#removePrimitive} public
     32     *
     33     * @param <N> The node type
     34     * @param <W> The way type
     35     * @param <R> The relation type
     36     */
     37    static class LocalQuadBucketPrimitiveStore<N extends INode, W extends IWay<N>, R extends IRelation<?>>
     38      extends QuadBucketPrimitiveStore<N, W, R> {
     39        // Allow us to remove primitives (protected in {@link QuadBucketPrimitiveStore})
     40        @Override
     41        public void removePrimitive(IPrimitive primitive) {
     42            super.removePrimitive(primitive);
     43        }
     44    }
     45
     46    protected final int zoom;
     47    protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
     48    protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
     49    protected final Set<Tile> addedTiles = new HashSet<>();
     50    protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
     51      .foreignKey(new Storage.PrimitiveIdHash());
     52    protected final Collection<DataSource> dataSources = new LinkedList<>();
     53    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
     54
     55    DataStore(int zoom) {
     56        this.zoom = zoom;
     57    }
     58
     59    public int getZoom() {
     60        return this.zoom;
     61    }
     62
     63    public QuadBucketPrimitiveStore<N, W, R> getStore() {
     64        return this.store;
     65    }
     66
     67    public Storage<O> getAllPrimitives() {
     68        return this.allPrimitives;
     69    }
     70
     71    public Map<PrimitiveId, O> getPrimitivesMap() {
     72        if (this.readWriteLock.isWriteLocked()) {
     73            return new HashMap<>(this.primitivesMap);
     74        }
     75        return this.primitivesMap;
     76    }
     77
     78    public Collection<DataSource> getDataSources() {
     79        return Collections.unmodifiableCollection(dataSources);
     80    }
     81
     82    /**
     83     * Add a datasource to this data set
     84     * @param dataSource The data soure to add
     85     */
     86    public void addDataSource(DataSource dataSource) {
     87        this.dataSources.add(dataSource);
     88    }
     89
     90    /**
     91     * Add a primitive to this dataset
     92     * @param primitive The primitive to remove
     93     */
     94    @SuppressWarnings("squid:S2445")
     95    protected void removePrimitive(O primitive) {
     96        if (primitive == null) {
     97            return;
     98        }
     99        // This is deliberate -- attempting to remove the primitive twice causes issues
     100        synchronized (primitive) {
     101            if (this.allPrimitives.contains(primitive)) {
     102                this.store.removePrimitive(primitive);
     103                this.allPrimitives.remove(primitive);
     104                this.primitivesMap.remove(primitive.getPrimitiveId());
     105            }
     106        }
     107    }
     108
     109    /**
     110     * Add a primitive to this dataset
     111     * @param primitive The primitive to add
     112     */
     113    protected void addPrimitive(O primitive) {
     114        this.store.addPrimitive(primitive);
     115        this.allPrimitives.add(primitive);
     116        this.primitivesMap.put(primitive.getPrimitiveId(), primitive);
     117    }
     118
     119    /**
     120     * Get the read/write lock for this dataset
     121     * @return The read/write lock
     122     */
     123    protected ReentrantReadWriteLock getReadWriteLock() {
     124        return this.readWriteLock;
     125    }
     126}
  • new file src/org/openstreetmap/josm/data/vector/VectorDataSet.java

    diff --git src/org/openstreetmap/josm/data/vector/VectorDataSet.java src/org/openstreetmap/josm/data/vector/VectorDataSet.java
    new file mode 100644
    index 000000000..dfa9334a3
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.util.ArrayList;
     5import java.util.Arrays;
     6import java.util.Collection;
     7import java.util.Collections;
     8import java.util.HashSet;
     9import java.util.List;
     10import java.util.Map;
     11import java.util.Objects;
     12import java.util.Optional;
     13import java.util.concurrent.ConcurrentHashMap;
     14import java.util.concurrent.locks.Lock;
     15import java.util.concurrent.locks.ReentrantReadWriteLock;
     16import java.util.function.Predicate;
     17import java.util.function.Supplier;
     18import java.util.stream.Collectors;
     19import java.util.stream.IntStream;
     20import java.util.stream.Stream;
     21
     22import org.openstreetmap.gui.jmapviewer.Tile;
     23import org.openstreetmap.josm.data.DataSource;
     24import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     25import org.openstreetmap.josm.data.osm.BBox;
     26import org.openstreetmap.josm.data.osm.DataSelectionListener;
     27import org.openstreetmap.josm.data.osm.DownloadPolicy;
     28import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     29import org.openstreetmap.josm.data.osm.OsmData;
     30import org.openstreetmap.josm.data.osm.PrimitiveId;
     31import org.openstreetmap.josm.data.osm.UploadPolicy;
     32import org.openstreetmap.josm.data.osm.WaySegment;
     33import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     34import org.openstreetmap.josm.tools.ListenerList;
     35import org.openstreetmap.josm.tools.Logging;
     36import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     37
     38/**
     39 * A data class for Vector Data
     40 *
     41 * @author Taylor Smock
     42 * @since xxx
     43 */
     44public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
     45    // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
     46    // for new values (perf increase). See JDK-8161372 for more info.
     47    private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>();
     48    private final Collection<PrimitiveId> selected = new HashSet<>();
     49    // Both of these listener lists are useless, since they expect OsmPrimitives at this time
     50    private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
     51    private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
     52    private boolean lock = true;
     53    private String name;
     54    private short mappaintCacheIdx = 1;
     55
     56    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
     57
     58    /**
     59     * The distance to consider nodes duplicates -- mostly a memory saving measure.
     60     * 0.000_000_1 ~1.2 cm (+- 5.57 mm)
     61     * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a>
     62     * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate
     63     * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision)
     64     */
     65    protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f;
     66
     67    /**
     68     * The current zoom we are getting/adding to
     69     */
     70    private int zoom;
     71    /**
     72     * Default to normal download policy
     73     */
     74    private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL;
     75    /**
     76     * Default to a blocked upload policy
     77     */
     78    private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED;
     79    /**
     80     * The paint style for this layer
     81     */
     82    private ElemStyles styles;
     83
     84    @Override
     85    public Collection<DataSource> getDataSources() {
     86        final int currentZoom = this.zoom;
     87        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
     88        return dataStore.getDataSources();
     89    }
     90
     91    /**
     92     * Add a data source
     93     *
     94     * @param currentZoom the zoom
     95     * @param dataSource  The datasource to add at the zoom level
     96     */
     97    public void addDataSource(int currentZoom, DataSource dataSource) {
     98        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
     99        dataStore.addDataSource(dataSource);
     100    }
     101
     102    @Override
     103    public void lock() {
     104        this.lock = true;
     105    }
     106
     107    @Override
     108    public void unlock() {
     109        this.lock = false;
     110    }
     111
     112    @Override
     113    public boolean isLocked() {
     114        return this.lock;
     115    }
     116
     117    @Override
     118    public String getVersion() {
     119        return "8"; // TODO get this dynamically. Not critical, as this is currently the _only_ version.
     120    }
     121
     122    @Override
     123    public String getName() {
     124        return this.name;
     125    }
     126
     127    @Override
     128    public void setName(String name) {
     129        this.name = name;
     130    }
     131
     132    @Override
     133    public void addPrimitive(VectorPrimitive primitive) {
     134        primitive.setDataSet(this);
     135        final int currentZoom = this.zoom;
     136        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
     137        tryWrite(dataStore, () -> dataStore.addPrimitive(primitive));
     138    }
     139
     140    /**
     141     * Remove a primitive from this dataset
     142     *
     143     * @param primitive The primitive to remove
     144     */
     145    protected void removePrimitive(VectorPrimitive primitive) {
     146        if (primitive.getDataSet() == this) {
     147            primitive.setDataSet(null);
     148            this.dataStoreMap.values()
     149              .forEach(vectorDataStore -> tryWrite(vectorDataStore, () -> vectorDataStore.removePrimitive(primitive)));
     150        }
     151    }
     152
     153    @Override
     154    public void clear() {
     155        synchronized (this.dataStoreMap) {
     156            this.dataStoreMap.clear();
     157        }
     158    }
     159
     160    @Override
     161    public List<VectorNode> searchNodes(BBox bbox) {
     162        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchNodes(bbox))
     163          .orElseGet(Collections::emptyList);
     164    }
     165
     166    @Override
     167    public boolean containsNode(VectorNode vectorNode) {
     168        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsNode(vectorNode)).orElse(false);
     169    }
     170
     171    @Override
     172    public List<VectorWay> searchWays(BBox bbox) {
     173        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchWays(bbox))
     174          .orElseGet(Collections::emptyList);
     175    }
     176
     177    @Override
     178    public boolean containsWay(VectorWay vectorWay) {
     179        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsWay(vectorWay)).orElse(false);
     180    }
     181
     182    @Override
     183    public List<VectorRelation> searchRelations(BBox bbox) {
     184        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchRelations(bbox))
     185          .orElseGet(Collections::emptyList);
     186    }
     187
     188    @Override
     189    public boolean containsRelation(VectorRelation vectorRelation) {
     190        return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsRelation(vectorRelation)).orElse(false);
     191    }
     192
     193    @Override
     194    public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
     195        return this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).map(m -> m .get(primitiveId)).orElse(null);
     196    }
     197
     198    // The last return statement is "unchecked", even though it is literally the same as the previous return, except
     199    // as an optional.
     200    @SuppressWarnings("unchecked")
     201    @Override
     202    public <T extends VectorPrimitive> Collection<T> getPrimitives(
     203      Predicate<? super VectorPrimitive> predicate) {
     204        final VectorDataStore dataStore = this.getBestZoomDataStore().orElse(null);
     205        if (dataStore == null) {
     206            return Collections.emptyList();
     207        }
     208
     209        if (dataStore.getReadWriteLock().isWriteLocked()) {
     210            return new SubclassFilteredCollection<>(new HashSet<>(dataStore.getAllPrimitives()), predicate);
     211        }
     212        return (Collection<T>) tryRead(dataStore, () -> new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate))
     213          // Throw an NPE if we don't have a collection (this should never happen, so if it does, _something_ is wrong)
     214          .orElseThrow(NullPointerException::new);
     215    }
     216
     217    @Override
     218    public Collection<VectorNode> getNodes() {
     219        return this.getPrimitives(VectorNode.class::isInstance);
     220    }
     221
     222    @Override
     223    public Collection<VectorWay> getWays() {
     224        return this.getPrimitives(VectorWay.class::isInstance);
     225    }
     226
     227    @Override
     228    public Collection<VectorRelation> getRelations() {
     229        return this.getPrimitives(VectorRelation.class::isInstance);
     230    }
     231
     232    @Override
     233    public DownloadPolicy getDownloadPolicy() {
     234        return this.downloadPolicy;
     235    }
     236
     237    @Override
     238    public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
     239        this.downloadPolicy = downloadPolicy;
     240    }
     241
     242    @Override
     243    public UploadPolicy getUploadPolicy() {
     244        return this.uploadPolicy;
     245    }
     246
     247    @Override
     248    public void setUploadPolicy(UploadPolicy uploadPolicy) {
     249        this.uploadPolicy = uploadPolicy;
     250    }
     251
     252    /**
     253     * Get the current Read/Write lock
     254     * @implNote This changes based off of zoom level. Please do not use this in a finally block
     255     * @return The current read/write lock
     256     */
     257    @Override
     258    public Lock getReadLock() {
     259        return getBestZoomDataStore().map(VectorDataStore::getReadWriteLock).map(ReentrantReadWriteLock::readLock)
     260          .orElse(this.readWriteLock.readLock());
     261    }
     262
     263    @Override
     264    public Collection<WaySegment> getHighlightedVirtualNodes() {
     265        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
     266        return Collections.emptyList();
     267    }
     268
     269    @Override
     270    public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
     271        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
     272    }
     273
     274    @Override
     275    public Collection<WaySegment> getHighlightedWaySegments() {
     276        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
     277        return Collections.emptyList();
     278    }
     279
     280    @Override
     281    public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
     282        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
     283    }
     284
     285    @Override
     286    public void addHighlightUpdateListener(HighlightUpdateListener listener) {
     287        this.highlightUpdateListenerListenerList.addListener(listener);
     288    }
     289
     290    @Override
     291    public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
     292        this.highlightUpdateListenerListenerList.removeListener(listener);
     293    }
     294
     295    @Override
     296    public Collection<VectorPrimitive> getAllSelected() {
     297        final Optional<VectorDataStore> dataStore = this.getBestZoomDataStore();
     298        return dataStore.map(vectorDataStore -> vectorDataStore.getAllPrimitives().stream()
     299          .filter(primitive -> this.selected.contains(primitive.getPrimitiveId()))
     300          .collect(Collectors.toList())).orElse(Collections.emptyList());
     301    }
     302
     303    /**
     304     * Get the best zoom datastore
     305     * @return A datastore with data, or {@code null} if no good datastore exists.
     306     */
     307    private Optional<VectorDataStore> getBestZoomDataStore() {
     308        final int currentZoom = this.zoom;
     309        if (this.dataStoreMap.containsKey(currentZoom)) {
     310            return Optional.of(this.dataStoreMap.get(currentZoom));
     311        }
     312        // Check up to two zooms higher (may cause perf hit)
     313        for (int tZoom = currentZoom + 1; tZoom < currentZoom + 3; tZoom++) {
     314            if (this.dataStoreMap.containsKey(tZoom)) {
     315                return Optional.of(this.dataStoreMap.get(tZoom));
     316            }
     317        }
     318        // Return *any* lower zoom data (shouldn't cause a perf hit...)
     319        for (int tZoom = currentZoom - 1; tZoom >= 0; tZoom--) {
     320            if (this.dataStoreMap.containsKey(tZoom)) {
     321                return Optional.of(this.dataStoreMap.get(tZoom));
     322            }
     323        }
     324        // Check higher level zooms. May cause perf issues if selected datastore has a lot of data.
     325        for (int tZoom = currentZoom + 3; tZoom < 34; tZoom++) {
     326            if (this.dataStoreMap.containsKey(tZoom)) {
     327                return Optional.of(this.dataStoreMap.get(tZoom));
     328            }
     329        }
     330        return Optional.empty();
     331    }
     332
     333    @Override
     334    public boolean selectionEmpty() {
     335        return this.selected.isEmpty();
     336    }
     337
     338    @Override
     339    public boolean isSelected(VectorPrimitive osm) {
     340        return this.selected.contains(osm.getPrimitiveId());
     341    }
     342
     343    @Override
     344    public void toggleSelected(Collection<? extends PrimitiveId> osm) {
     345        this.toggleSelectedImpl(osm.stream());
     346    }
     347
     348    @Override
     349    public void toggleSelected(PrimitiveId... osm) {
     350        this.toggleSelectedImpl(Stream.of(osm));
     351    }
     352
     353    private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
     354        osm.forEach(primitiveId -> {
     355            if (this.selected.contains(primitiveId)) {
     356                this.selected.remove(primitiveId);
     357            } else {
     358                this.selected.add(primitiveId);
     359            }
     360        });
     361    }
     362
     363    @Override
     364    public void setSelected(Collection<? extends PrimitiveId> selection) {
     365        this.setSelectedImpl(selection.stream());
     366    }
     367
     368    @Override
     369    public void setSelected(PrimitiveId... osm) {
     370        this.setSelectedImpl(Stream.of(osm));
     371    }
     372
     373    private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
     374        this.selected.clear();
     375        osm.forEach(this.selected::add);
     376    }
     377
     378    @Override
     379    public void addSelected(Collection<? extends PrimitiveId> selection) {
     380        this.addSelectedImpl(selection.stream());
     381    }
     382
     383    @Override
     384    public void addSelected(PrimitiveId... osm) {
     385        this.addSelectedImpl(Stream.of(osm));
     386    }
     387
     388    private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
     389        osm.forEach(this.selected::add);
     390    }
     391
     392    @Override
     393    public void clearSelection(PrimitiveId... osm) {
     394        this.clearSelectionImpl(Stream.of(osm));
     395    }
     396
     397    @Override
     398    public void clearSelection(Collection<? extends PrimitiveId> list) {
     399        this.clearSelectionImpl(list.stream());
     400    }
     401
     402    @Override
     403    public void clearSelection() {
     404        this.clearSelectionImpl(new ArrayList<>(this.selected).stream());
     405    }
     406
     407    private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
     408        osm.forEach(this.selected::remove);
     409    }
     410
     411    @Override
     412    public void addSelectionListener(DataSelectionListener listener) {
     413        this.dataSelectionListenerListenerList.addListener(listener);
     414    }
     415
     416    @Override
     417    public void removeSelectionListener(DataSelectionListener listener) {
     418        this.dataSelectionListenerListenerList.removeListener(listener);
     419    }
     420
     421    public short getMappaintCacheIndex() {
     422        return this.mappaintCacheIdx;
     423    }
     424
     425    @Override
     426    public void clearMappaintCache() {
     427        this.mappaintCacheIdx++;
     428    }
     429
     430    public void setZoom(int zoom) {
     431        if (zoom == this.zoom) {
     432            return; // Do nothing -- zoom isn't actually changing
     433        }
     434        this.zoom = zoom;
     435        this.clearMappaintCache();
     436        final int[] nearestZoom = {-1, -1, -1, -1};
     437        nearestZoom[0] = zoom;
     438        // Create a new list to avoid concurrent modification issues
     439        synchronized (this.dataStoreMap) {
     440            final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull)
     441              .mapToInt(Integer::intValue).sorted().toArray();
     442            final int index;
     443            if (this.dataStoreMap.containsKey(zoom)) {
     444                index = Arrays.binarySearch(keys, zoom);
     445            } else {
     446                // (-(insertion point) - 1) = return -> insertion point = -(return + 1)
     447                index = -(Arrays.binarySearch(keys, zoom) + 1);
     448            }
     449            if (index > 0) {
     450                nearestZoom[1] = keys[index - 1];
     451            }
     452            if (index < keys.length - 2) {
     453                nearestZoom[2] = keys[index + 1];
     454            }
     455
     456            nearestZoom[3] = this.getBestZoomDataStore().map(VectorDataStore::getZoom).orElse(-1);
     457            IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
     458              .mapToObj(this.dataStoreMap::get).forEach(VectorDataStore::destroy);
     459            IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
     460              .forEach(this.dataStoreMap::remove);
     461        }
     462    }
     463
     464    public int getZoom() {
     465        return this.zoom;
     466    }
     467
     468    /**
     469     * Add tile data to this dataset
     470     * @param tile The tile to add
     471     * @param <T> The tile type
     472     */
     473    public <T extends Tile & VectorTile> void addTileData(T tile) {
     474        final int currentZoom = tile.getZoom();
     475        // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
     476        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
     477        tryWrite(dataStore, () -> dataStore.addTile(tile));
     478    }
     479
     480    /**
     481     * Try to read something (here to avoid boilerplate)
     482     *
     483     * @param supplier The reading function
     484     * @param <T>      The return type
     485     * @return The optional return
     486     */
     487    private static <T> Optional<T> tryRead(VectorDataStore dataStore, Supplier<T> supplier) {
     488        try {
     489            dataStore.getReadWriteLock().readLock().lockInterruptibly();
     490            return Optional.ofNullable(supplier.get());
     491        } catch (InterruptedException e) {
     492            Logging.error(e);
     493            Thread.currentThread().interrupt();
     494        } finally {
     495            dataStore.getReadWriteLock().readLock().unlock();
     496        }
     497        return Optional.empty();
     498    }
     499
     500    /**
     501     * Try to write something (here to avoid boilerplate)
     502     *
     503     * @param runnable The writing function
     504     */
     505    private static void tryWrite(VectorDataStore dataStore, Runnable runnable) {
     506        try {
     507            dataStore.getReadWriteLock().writeLock().lockInterruptibly();
     508            runnable.run();
     509        } catch (InterruptedException e) {
     510            Logging.error(e);
     511            Thread.currentThread().interrupt();
     512        } finally {
     513            if (dataStore.getReadWriteLock().isWriteLockedByCurrentThread()) {
     514                dataStore.getReadWriteLock().writeLock().unlock();
     515            }
     516        }
     517    }
     518
     519    /**
     520     * Get the styles for this layer
     521     *
     522     * @return The styles
     523     */
     524    public ElemStyles getStyles() {
     525        return this.styles;
     526    }
     527
     528    /**
     529     * Set the styles for this layer
     530     * @param styles The styles to set for this layer
     531     */
     532    public void setStyles(Collection<ElemStyles> styles) {
     533        if (styles.size() == 1) {
     534            this.styles = styles.iterator().next();
     535        } else if (!styles.isEmpty()) {
     536            this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList()));
     537        } else {
     538            this.styles = null;
     539        }
     540    }
     541}
  • new file src/org/openstreetmap/josm/data/vector/VectorDataStore.java

    diff --git src/org/openstreetmap/josm/data/vector/VectorDataStore.java src/org/openstreetmap/josm/data/vector/VectorDataStore.java
    new file mode 100644
    index 000000000..f486651b6
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.awt.geom.Area;
     5import java.awt.geom.Ellipse2D;
     6import java.awt.geom.Path2D;
     7import java.awt.geom.PathIterator;
     8import java.util.ArrayList;
     9import java.util.Collection;
     10import java.util.Collections;
     11import java.util.List;
     12import java.util.Objects;
     13import java.util.Optional;
     14import java.util.stream.Collectors;
     15
     16import org.openstreetmap.gui.jmapviewer.Coordinate;
     17import org.openstreetmap.gui.jmapviewer.Tile;
     18import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     19import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     20import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     21import org.openstreetmap.josm.data.osm.BBox;
     22import org.openstreetmap.josm.data.osm.INode;
     23import org.openstreetmap.josm.data.osm.IRelation;
     24import org.openstreetmap.josm.data.osm.IWay;
     25import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     26import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
     27import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     28import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
     29import org.openstreetmap.josm.tools.Destroyable;
     30import org.openstreetmap.josm.tools.Geometry;
     31
     32/**
     33 * A data store for Vector Data sets
     34 * @author Taylor Smock
     35 * @since xxx
     36 */
     37class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
     38    private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
     39    private final VectorDataSet dataSet;
     40
     41    VectorDataStore(VectorDataSet dataSet, int zoom) {
     42        super(zoom);
     43        this.dataSet = dataSet;
     44    }
     45
     46    @Override
     47    protected void addPrimitive(VectorPrimitive primitive) {
     48        primitive.setDataSet(this.dataSet);
     49        // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways)
     50        if (primitive.getUniqueId() == 0) {
     51            final UniqueIdGenerator generator = primitive.getIdGenerator();
     52            long id;
     53            do {
     54                id = generator.generateUniqueId();
     55            } while (this.primitivesMap.containsKey(new SimplePrimitiveId(id, primitive.getType())));
     56            primitive.setId(primitive.getIdGenerator().generateUniqueId());
     57        }
     58        if (primitive instanceof VectorRelation && !primitive.isMultipolygon()) {
     59            primitive = mergeWays((VectorRelation) primitive);
     60        }
     61        final VectorPrimitive alreadyAdded = this.primitivesMap.get(primitive.getPrimitiveId());
     62        final VectorRelation mergedRelation = (VectorRelation) this.primitivesMap
     63          .get(new SimplePrimitiveId(primitive.getPrimitiveId().getUniqueId(),
     64            OsmPrimitiveType.RELATION));
     65        if (alreadyAdded == null || alreadyAdded.equals(primitive)) {
     66            super.addPrimitive(primitive);
     67        } else if (mergedRelation != null && mergedRelation.get(JOSM_MERGE_TYPE_KEY) != null) {
     68            mergedRelation.addRelationMember(new VectorRelationMember("", primitive));
     69            super.addPrimitive(primitive);
     70            // Check that all primitives can be merged
     71            if (mergedRelation.getMemberPrimitivesList().stream().allMatch(IWay.class::isInstance)) {
     72                // This pretty much does the "right" thing
     73                this.mergeWays(mergedRelation);
     74            } else if (!(primitive instanceof IWay)) {
     75                // Can't merge, ever (one of the childs is a node/relation)
     76                mergedRelation.remove(JOSM_MERGE_TYPE_KEY);
     77            }
     78        } else if (mergedRelation != null && primitive instanceof IRelation) {
     79            // Just add to the relation
     80            ((VectorRelation) primitive).getMembers().forEach(mergedRelation::addRelationMember);
     81        } else if (alreadyAdded instanceof VectorWay && primitive instanceof VectorWay) {
     82            final VectorRelation temporaryRelation =
     83              mergedRelation == null ? new VectorRelation(primitive.getLayer()) : mergedRelation;
     84            if (mergedRelation == null) {
     85                temporaryRelation.put(JOSM_MERGE_TYPE_KEY, "merge");
     86                temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded));
     87            }
     88            temporaryRelation.addRelationMember(new VectorRelationMember("", primitive));
     89            temporaryRelation.setDataSet(this.dataSet);
     90            super.addPrimitive(primitive);
     91            super.addPrimitive(temporaryRelation);
     92        }
     93    }
     94
     95    private VectorPrimitive mergeWays(VectorRelation relation) {
     96        List<VectorRelationMember> members = RelationSorter.sortMembersByConnectivity(relation.getMembers());
     97        Collection<VectorWay> relationWayList = members.stream().map(VectorRelationMember::getMember)
     98          .filter(VectorWay.class::isInstance)
     99          .map(VectorWay.class::cast).collect(Collectors.toCollection(ArrayList::new));
     100        // Only support way-only relations
     101        if (relationWayList.size() != relation.getMemberPrimitivesList().size()) {
     102            return relation;
     103        }
     104        List<VectorWay> wayList = new ArrayList<>(relation.getMembersCount());
     105        // Assume that the order may not be correct, worst case O(n), best case O(n/2)
     106        // Assume that the ways were drawn in order
     107        final int maxIteration = relationWayList.size();
     108        int iteration = 0;
     109        while (iteration < maxIteration && wayList.size() < relationWayList.size()) {
     110            for (VectorWay way : relationWayList) {
     111                if (wayList.isEmpty()) {
     112                    wayList.add(way);
     113                    continue;
     114                }
     115                // Check first/last ways (last first, since the list *should* be sorted)
     116                if (canMergeWays(wayList.get(wayList.size() - 1), way, false)) {
     117                    wayList.add(way);
     118                } else if (canMergeWays(wayList.get(0), way, false)) {
     119                    wayList.add(0, way);
     120                }
     121            }
     122            iteration++;
     123            relationWayList.removeIf(wayList::contains);
     124        }
     125        if (!relationWayList.isEmpty()) {
     126            return relation;
     127        }
     128        // Merge ways
     129        List<VectorNode> nodes = new ArrayList<>();
     130        for (VectorWay way : wayList) {
     131            for (VectorNode node : way.getNodes()) {
     132                if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) {
     133                    nodes.add(node);
     134                }
     135            }
     136        }
     137        VectorWay way = wayList.get(0);
     138        way.setNodes(nodes);
     139        wayList.remove(way);
     140        wayList.forEach(this::removePrimitive);
     141        this.removePrimitive(relation);
     142        return way;
     143    }
     144
     145    private static <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) {
     146        final List<N> nodes = new ArrayList<>(old.getNodes());
     147        boolean added = true;
     148        if (allowReverse && old.firstNode().equals(toAdd.firstNode())) {
     149            // old <-|-> new becomes old ->|-> new
     150            Collections.reverse(nodes);
     151            nodes.addAll(toAdd.getNodes());
     152        } else if (old.firstNode().equals(toAdd.lastNode())) {
     153            // old <-|<- new, so we prepend the new nodes in order
     154            nodes.addAll(0, toAdd.getNodes());
     155        } else if (old.lastNode().equals(toAdd.firstNode())) {
     156            // old ->|-> new, we just add it
     157            nodes.addAll(toAdd.getNodes());
     158        } else if (allowReverse && old.lastNode().equals(toAdd.lastNode())) {
     159            // old ->|<- new, we need to reverse new
     160            final List<N> toAddNodes = new ArrayList<>(toAdd.getNodes());
     161            Collections.reverse(toAddNodes);
     162            nodes.addAll(toAddNodes);
     163        } else {
     164            added = false;
     165        }
     166        if (added) {
     167            // This is (technically) always correct
     168            old.setNodes(nodes);
     169        }
     170        return added;
     171    }
     172
     173    private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer,
     174      Collection<VectorPrimitive> featureObjects, int x, int y) {
     175        final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
     176        final int layerExtent = layer.getExtent() * 2;
     177        final ICoordinate lowerRight = tile.getTileSource()
     178          .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     179        final ICoordinate coords = new Coordinate(
     180          upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent,
     181          upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent);
     182        final Collection<VectorNode> nodes = this.store
     183          .searchNodes(new BBox(coords.getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE));
     184        final VectorNode node;
     185        if (!nodes.isEmpty()) {
     186            final VectorNode first = nodes.iterator().next();
     187            if (first.isDisabled() || !first.isVisible()) {
     188                // Only replace nodes that are not visible
     189                node = new VectorNode(layer.getName());
     190                node.setCoor(node.getCoor());
     191                first.getReferrers(true).forEach(primitive -> {
     192                    if (primitive instanceof VectorWay) {
     193                        List<VectorNode> nodeList = new ArrayList<>(((VectorWay) primitive).getNodes());
     194                        nodeList.replaceAll(vnode -> vnode.equals(first) ? node : vnode);
     195                        ((VectorWay) primitive).setNodes(nodeList);
     196                    } else if (primitive instanceof VectorRelation) {
     197                        List<VectorRelationMember> members = new ArrayList<>(((VectorRelation) primitive).getMembers());
     198                        members.replaceAll(member ->
     199                          member.getMember().equals(first) ? new VectorRelationMember(member.getRole(), node) : member);
     200                        ((VectorRelation) primitive).setMembers(members);
     201                    }
     202                });
     203                this.removePrimitive(first);
     204            } else {
     205                node = first;
     206            }
     207        } else {
     208            node = new VectorNode(layer.getName());
     209        }
     210        node.setCoor(coords);
     211        featureObjects.add(node);
     212        return node;
     213    }
     214
     215    private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer,
     216      Collection<VectorPrimitive> featureObjects, Path2D shape) {
     217        final PathIterator pathIterator = shape.getPathIterator(null);
     218        final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream()
     219          .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(
     220            Collectors.toList());
     221        // These nodes technically do not exist, so we shouldn't show them
     222        ways.stream().flatMap(way -> way.getNodes().stream())
     223          .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0)
     224          .forEach(prim -> {
     225              prim.setDisabled(true);
     226              prim.setVisible(false);
     227          });
     228        return ways;
     229    }
     230
     231    private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer,
     232      Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) {
     233        final List<VectorNode> nodes = new ArrayList<>();
     234        final double[] coords = new double[6];
     235        final List<VectorPrimitive> ways = new ArrayList<>();
     236        do {
     237            final int type = pathIterator.currentSegment(coords);
     238            pathIterator.next();
     239            if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) {
     240                if (PathIterator.SEG_CLOSE == type) {
     241                    nodes.add(nodes.get(0));
     242                }
     243                // New line
     244                if (!nodes.isEmpty()) {
     245                    final VectorWay way = new VectorWay(layer.getName());
     246                    way.setNodes(nodes);
     247                    featureObjects.add(way);
     248                    ways.add(way);
     249                }
     250                nodes.clear();
     251            }
     252            if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) {
     253                final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]);
     254                nodes.add(node);
     255            } else if (PathIterator.SEG_CLOSE != type) {
     256                // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time.
     257                throw new UnsupportedOperationException();
     258            }
     259        } while (!pathIterator.isDone());
     260        if (!nodes.isEmpty()) {
     261            final VectorWay way = new VectorWay(layer.getName());
     262            way.setNodes(nodes);
     263            featureObjects.add(way);
     264            ways.add(way);
     265        }
     266        return ways;
     267    }
     268
     269    private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer,
     270      Collection<VectorPrimitive> featureObjects, Area area) {
     271        final PathIterator pathIterator = area.getPathIterator(null);
     272        final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator);
     273        VectorRelation vectorRelation = new VectorRelation(layer.getName());
     274        for (VectorPrimitive member : members) {
     275            final String role;
     276            if (member instanceof VectorWay && ((VectorWay) member).isClosed()) {
     277                role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner";
     278            } else {
     279                role = "";
     280            }
     281            vectorRelation.addRelationMember(new VectorRelationMember(role, member));
     282        }
     283        return vectorRelation;
     284    }
     285
     286    /**
     287     * Add a tile to this data store
     288     * @param tile The tile to add
     289     * @param <T> The tile type
     290     */
     291    public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
     292        Optional<Tile> previous = this.addedTiles.stream()
     293          .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
     294        // Check if we have already added the tile (just to save processing time)
     295        if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
     296            previous.ifPresent(this.addedTiles::remove);
     297            this.addedTiles.add(tile);
     298            for (Layer layer : tile.getLayers()) {
     299                layer.getFeatures().forEach(feature -> {
     300                    org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
     301                      .getGeometryObject();
     302                    List<VectorPrimitive> featureObjects = new ArrayList<>();
     303                    List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
     304                    geometry.getShapes().forEach(shape -> {
     305                        final VectorPrimitive primitive;
     306                        if (shape instanceof Ellipse2D) {
     307                            primitive = pointToNode(tile, layer, featureObjects,
     308                              (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
     309                        } else if (shape instanceof Path2D) {
     310                            primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst()
     311                              .orElse(null);
     312                        } else if (shape instanceof Area) {
     313                            primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
     314                            primitive.put("type", "multipolygon");
     315                        } else {
     316                            // We shouldn't hit this, but just in case
     317                            throw new UnsupportedOperationException();
     318                        }
     319                        primaryFeatureObjects.add(primitive);
     320                    });
     321                    final VectorPrimitive primitive;
     322                    if (primaryFeatureObjects.size() == 1) {
     323                        primitive = primaryFeatureObjects.get(0);
     324                        if (primitive instanceof IRelation && !primitive.isMultipolygon()) {
     325                            primitive.put(JOSM_MERGE_TYPE_KEY, "merge");
     326                        }
     327                    } else if (!primaryFeatureObjects.isEmpty()) {
     328                        VectorRelation relation = new VectorRelation(layer.getName());
     329                        primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim))
     330                          .forEach(relation::addRelationMember);
     331                        primitive = relation;
     332                    } else {
     333                        return;
     334                    }
     335                    primitive.setId(feature.getId());
     336                    feature.getTags().forEach(primitive::put);
     337                    featureObjects.forEach(this::addPrimitive);
     338                    primaryFeatureObjects.forEach(this::addPrimitive);
     339                    this.addPrimitive(primitive);
     340                });
     341            }
     342        }
     343    }
     344
     345    @Override
     346    public void destroy() {
     347        this.addedTiles.forEach(tile -> tile.setLoaded(false));
     348        this.addedTiles.forEach(tile -> tile.setImage(null));
     349        this.addedTiles.clear();
     350        this.store.clear();
     351        this.allPrimitives.clear();
     352        this.primitivesMap.clear();
     353    }
     354}
  • new file src/org/openstreetmap/josm/data/vector/VectorNode.java

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

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

    diff --git src/org/openstreetmap/josm/data/vector/VectorRelation.java src/org/openstreetmap/josm/data/vector/VectorRelation.java
    new file mode 100644
    index 000000000..0deb57e57
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.util.ArrayList;
     5import java.util.Collections;
     6import java.util.List;
     7
     8import org.openstreetmap.josm.data.osm.BBox;
     9import org.openstreetmap.josm.data.osm.IPrimitive;
     10import org.openstreetmap.josm.data.osm.IRelation;
     11import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     12import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     13import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     14
     15/**
     16 * The "Relation" type for vectors
     17 *
     18 * @author Taylor Smock
     19 * @since xxx
     20 */
     21public class VectorRelation extends VectorPrimitive implements IRelation<VectorRelationMember> {
     22    private static final UniqueIdGenerator RELATION_ID_GENERATOR = new UniqueIdGenerator();
     23    private final List<VectorRelationMember> members = new ArrayList<>();
     24    private BBox cachedBBox;
     25
     26    /**
     27     * Create a new relation for a layer
     28     * @param layer The layer the relation will belong to
     29     */
     30    public VectorRelation(String layer) {
     31        super(layer);
     32    }
     33
     34    @Override
     35    public UniqueIdGenerator getIdGenerator() {
     36        return RELATION_ID_GENERATOR;
     37    }
     38
     39    @Override
     40    public void accept(PrimitiveVisitor visitor) {
     41        visitor.visit(this);
     42    }
     43
     44    @Override
     45    public BBox getBBox() {
     46        if (cachedBBox == null) {
     47            cachedBBox = new BBox();
     48            for (IPrimitive member : this.getMemberPrimitivesList()) {
     49                cachedBBox.add(member.getBBox());
     50            }
     51        }
     52        return cachedBBox;
     53    }
     54
     55    protected void addRelationMember(VectorRelationMember member) {
     56        this.members.add(member);
     57        member.getMember().addReferrer(this);
     58        cachedBBox = null;
     59    }
     60
     61    /**
     62     * Remove the first instance of a member from the relation
     63     *
     64     * @param member The member to remove
     65     */
     66    protected void removeRelationMember(VectorRelationMember member) {
     67        this.members.remove(member);
     68        if (!this.members.contains(member)) {
     69            member.getMember().removeReferrer(this);
     70        }
     71    }
     72
     73    @Override
     74    public int getMembersCount() {
     75        return this.members.size();
     76    }
     77
     78    @Override
     79    public VectorRelationMember getMember(int index) {
     80        return this.members.get(index);
     81    }
     82
     83    @Override
     84    public List<VectorRelationMember> getMembers() {
     85        return Collections.unmodifiableList(this.members);
     86    }
     87
     88    @Override
     89    public void setMembers(List<VectorRelationMember> members) {
     90        this.members.clear();
     91        this.members.addAll(members);
     92    }
     93
     94    @Override
     95    public long getMemberId(int idx) {
     96        return this.getMember(idx).getMember().getId();
     97    }
     98
     99    @Override
     100    public String getRole(int idx) {
     101        return this.getMember(idx).getRole();
     102    }
     103
     104    @Override
     105    public OsmPrimitiveType getMemberType(int idx) {
     106        return this.getMember(idx).getType();
     107    }
     108
     109    @Override
     110    public OsmPrimitiveType getType() {
     111        return this.getMembers().stream().map(VectorRelationMember::getType)
     112          .allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
     113    }
     114}
  • new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java

    diff --git src/org/openstreetmap/josm/data/vector/VectorRelationMember.java src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
    new file mode 100644
    index 000000000..56d6dfe77
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.util.Optional;
     5
     6import org.openstreetmap.josm.data.osm.INode;
     7import org.openstreetmap.josm.data.osm.IRelation;
     8import org.openstreetmap.josm.data.osm.IRelationMember;
     9import org.openstreetmap.josm.data.osm.IWay;
     10import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     11import org.openstreetmap.josm.tools.CheckParameterUtil;
     12
     13/**
     14 * Relation members for a Vector Relation
     15 */
     16public class VectorRelationMember implements IRelationMember<VectorPrimitive> {
     17    private final String role;
     18    private final VectorPrimitive member;
     19
     20    /**
     21     * Create a new relation member
     22     * @param role The role of the member
     23     * @param member The member primitive
     24     */
     25    public VectorRelationMember(String role, VectorPrimitive member) {
     26        CheckParameterUtil.ensureParameterNotNull(member, "member");
     27        this.role = Optional.ofNullable(role).orElse("").intern();
     28        this.member = member;
     29    }
     30
     31    @Override
     32    public String getRole() {
     33        return this.role;
     34    }
     35
     36    @Override
     37    public boolean isNode() {
     38        return this.member instanceof INode;
     39    }
     40
     41    @Override
     42    public boolean isWay() {
     43        return this.member instanceof IWay;
     44    }
     45
     46    @Override
     47    public boolean isRelation() {
     48        return this.member instanceof IRelation;
     49    }
     50
     51    @Override
     52    public VectorPrimitive getMember() {
     53        return this.member;
     54    }
     55
     56    @Override
     57    public long getUniqueId() {
     58        return this.member.getId();
     59    }
     60
     61    @Override
     62    public OsmPrimitiveType getType() {
     63        return this.member.getType();
     64    }
     65
     66    @Override
     67    public boolean isNew() {
     68        return this.member.isNew();
     69    }
     70}
  • new file src/org/openstreetmap/josm/data/vector/VectorWay.java

    diff --git src/org/openstreetmap/josm/data/vector/VectorWay.java src/org/openstreetmap/josm/data/vector/VectorWay.java
    new file mode 100644
    index 000000000..582fca2d4
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import java.util.ArrayList;
     5import java.util.Collections;
     6import java.util.List;
     7import java.util.stream.Collectors;
     8
     9import org.openstreetmap.josm.data.osm.BBox;
     10import org.openstreetmap.josm.data.osm.INode;
     11import org.openstreetmap.josm.data.osm.IWay;
     12import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     13import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     14import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     15
     16/**
     17 * The "Way" type for a Vector layer
     18 *
     19 * @author Taylor Smock
     20 * @since xxx
     21 */
     22public class VectorWay extends VectorPrimitive implements IWay<VectorNode> {
     23    private static final UniqueIdGenerator WAY_GENERATOR = new UniqueIdGenerator();
     24    private final List<VectorNode> nodes = new ArrayList<>();
     25    private BBox cachedBBox;
     26
     27    /**
     28     * Create a new way for a layer
     29     * @param layer The layer for the way
     30     */
     31    public VectorWay(String layer) {
     32        super(layer);
     33    }
     34
     35    @Override
     36    public UniqueIdGenerator getIdGenerator() {
     37        return WAY_GENERATOR;
     38    }
     39
     40    @Override
     41    public void accept(PrimitiveVisitor visitor) {
     42        visitor.visit(this);
     43    }
     44
     45    @Override
     46    public BBox getBBox() {
     47        if (cachedBBox == null) {
     48            cachedBBox = new BBox();
     49            for (INode node : this.getNodes()) {
     50                cachedBBox.add(node.getBBox());
     51            }
     52        }
     53        return cachedBBox;
     54    }
     55
     56    @Override
     57    public int getNodesCount() {
     58        return this.getNodes().size();
     59    }
     60
     61    @Override
     62    public VectorNode getNode(int index) {
     63        return this.getNodes().get(index);
     64    }
     65
     66    @Override
     67    public List<VectorNode> getNodes() {
     68        return Collections.unmodifiableList(this.nodes);
     69    }
     70
     71    @Override
     72    public void setNodes(List<VectorNode> nodes) {
     73        this.nodes.forEach(node -> node.removeReferrer(this));
     74        this.nodes.clear();
     75        nodes.forEach(node -> node.addReferrer(this));
     76        this.nodes.addAll(nodes);
     77        this.cachedBBox = null;
     78    }
     79
     80    @Override
     81    public List<Long> getNodeIds() {
     82        return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList());
     83    }
     84
     85    @Override
     86    public long getNodeId(int idx) {
     87        return this.getNodes().get(idx).getId();
     88    }
     89
     90    @Override
     91    public boolean isClosed() {
     92        return this.firstNode() != null && this.firstNode().equals(this.lastNode());
     93    }
     94
     95    @Override
     96    public VectorNode firstNode() {
     97        if (this.nodes.isEmpty()) {
     98            return null;
     99        }
     100        return this.getNode(0);
     101    }
     102
     103    @Override
     104    public VectorNode lastNode() {
     105        if (this.nodes.isEmpty()) {
     106            return null;
     107        }
     108        return this.getNode(this.getNodesCount() - 1);
     109    }
     110
     111    @Override
     112    public boolean isFirstLastNode(INode n) {
     113        if (this.nodes.isEmpty()) {
     114            return false;
     115        }
     116        return this.firstNode().equals(n) || this.lastNode().equals(n);
     117    }
     118
     119    @Override
     120    public boolean isInnerNode(INode n) {
     121        if (this.nodes.isEmpty()) {
     122            return false;
     123        }
     124        return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream()
     125          .anyMatch(vectorNode -> vectorNode.equals(n));
     126    }
     127
     128    @Override
     129    public OsmPrimitiveType getType() {
     130        return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
     131    }
     132}
  • src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
    index 0ac990275..1b768e6fb 100644
    import java.util.Set;  
    1010import java.util.TreeMap;
    1111import java.util.TreeSet;
    1212
    13 import org.openstreetmap.josm.data.osm.Node;
    14 import org.openstreetmap.josm.data.osm.RelationMember;
    15 import org.openstreetmap.josm.data.osm.Way;
     13import org.openstreetmap.josm.data.osm.INode;
     14import org.openstreetmap.josm.data.osm.IPrimitive;
     15import org.openstreetmap.josm.data.osm.IRelationMember;
     16import org.openstreetmap.josm.data.osm.IWay;
    1617
    1718/**
    1819 * Auxiliary class for relation sorting.
    import org.openstreetmap.josm.data.osm.Way;  
    2627 * (that are shared by other members).
    2728 *
    2829 * @author Christiaan Welvaart &lt;cjw@time4t.net&gt;
    29  * @since 1785
     30 * @param <T> The type of {@link IRelationMember}
     31 * @since 1785, xxx (generics)
    3032 */
    31 public class RelationNodeMap {
     33public class RelationNodeMap<T extends IRelationMember<? extends IPrimitive>> {
    3234
    3335    private static final String ROLE_BACKWARD = "backward";
    3436
    3537    private static class NodesWays {
    36         public final Map<Node, Set<Integer>> nodes = new TreeMap<>();
    37         public final Map<Integer, Set<Node>> ways = new TreeMap<>();
     38        public final Map<INode, Set<Integer>> nodes = new TreeMap<>();
     39        public final Map<Integer, Set<INode>> ways = new TreeMap<>();
    3840        public final boolean oneWay;
    3941
    4042        NodesWays(boolean oneWay) {
    public class RelationNodeMap {  
    5658     * Used to keep track of what members are done.
    5759     */
    5860    private final Set<Integer> remaining = new TreeSet<>();
    59     private final Map<Integer, Set<Node>> remainingOneway = new TreeMap<>();
     61    private final Map<Integer, Set<INode>> remainingOneway = new TreeMap<>();
    6062
    6163    /**
    6264     * All members that are incomplete or not a way
    public class RelationNodeMap {  
    6769     * Gets the start node of the member, respecting the direction role.
    6870     * @param m The relation member.
    6971     * @return <code>null</code> if the member is no way, the node otherwise.
     72     * @since xxx (generics)
    7073     */
    71     public static Node firstOnewayNode(RelationMember m) {
     74    public static INode firstOnewayNode(IRelationMember<?> m) {
    7275        if (!m.isWay()) return null;
    7376        if (ROLE_BACKWARD.equals(m.getRole())) {
    7477            return m.getWay().lastNode();
    public class RelationNodeMap {  
    8184     * @param m The relation member.
    8285     * @return <code>null</code> if the member is no way, the node otherwise.
    8386     */
    84     public static Node lastOnewayNode(RelationMember m) {
     87    public static INode lastOnewayNode(IRelationMember<?> m) {
    8588        if (!m.isWay()) return null;
    8689        if (ROLE_BACKWARD.equals(m.getRole())) {
    8790            return m.getWay().firstNode();
    public class RelationNodeMap {  
    8992        return m.getWay().lastNode();
    9093    }
    9194
    92     RelationNodeMap(List<RelationMember> members) {
     95    RelationNodeMap(List<T> members) {
    9396        for (int i = 0; i < members.size(); ++i) {
    94             RelationMember m = members.get(i);
     97            T m = members.get(i);
    9598            if (m.getMember().isIncomplete() || !m.isWay() || m.getWay().getNodesCount() < 2) {
    9699                notSortable.add(i);
    97100                continue;
    98101            }
    99102
    100             Way w = m.getWay();
     103            IWay<?> w = m.getWay();
    101104            if (RelationSortUtils.roundaboutType(w) != NONE) {
    102                 for (Node nd : w.getNodes()) {
     105                for (INode nd : w.getNodes()) {
    103106                    addPair(nd, i);
    104107                }
    105108            } else if (RelationSortUtils.isOneway(m)) {
    public class RelationNodeMap {  
    118121        remaining.addAll(map.ways.keySet());
    119122    }
    120123
    121     private void addPair(Node n, int i) {
     124    private void addPair(INode n, int i) {
    122125        map.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
    123126        map.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
    124127    }
    125128
    126     private void addNodeWayMap(Node n, int i) {
     129    private void addNodeWayMap(INode n, int i) {
    127130        onewayMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
    128131    }
    129132
    130     private void addWayNodeMap(Node n, int i) {
     133    private void addWayNodeMap(INode n, int i) {
    131134        onewayMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
    132135    }
    133136
    134     private void addNodeWayMapReverse(Node n, int i) {
     137    private void addNodeWayMapReverse(INode n, int i) {
    135138        onewayReverseMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
    136139    }
    137140
    138     private void addWayNodeMapReverse(Node n, int i) {
     141    private void addWayNodeMapReverse(INode n, int i) {
    139142        onewayReverseMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
    140143    }
    141144
    142     private void addRemainingForward(Node n, int i) {
     145    private void addRemainingForward(INode n, int i) {
    143146        remainingOneway.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
    144147    }
    145148
    146149    private Integer firstOneway;
    147     private Node lastOnewayNode;
    148     private Node firstCircular;
     150    private INode lastOnewayNode;
     151    private INode firstCircular;
    149152
    150153    /**
    151154     * Return a relation member that is linked to the member 'i', but has not been popped yet.
    public class RelationNodeMap {  
    158161        if (firstOneway != null) return popForwardOnewayPart(way);
    159162
    160163        if (map.ways.containsKey(way)) {
    161             for (Node n : map.ways.get(way)) {
     164            for (INode n : map.ways.get(way)) {
    162165                Integer i = deleteAndGetAdjacentNode(map, n);
    163166                if (i != null) return i;
    164167
    public class RelationNodeMap {  
    176179
    177180    private Integer popForwardOnewayPart(Integer way) {
    178181        if (onewayMap.ways.containsKey(way)) {
    179             Node exitNode = onewayMap.ways.get(way).iterator().next();
     182            INode exitNode = onewayMap.ways.get(way).iterator().next();
    180183
    181184            if (checkIfEndOfLoopReached(exitNode)) {
    182185                lastOnewayNode = exitNode;
    public class RelationNodeMap {  
    201204    // Check if the given node can be the end of the loop (i.e. it has
    202205    // an outgoing bidirectional or multiple outgoing oneways, or we
    203206    // looped back to our first circular node)
    204     private boolean checkIfEndOfLoopReached(Node n) {
     207    private boolean checkIfEndOfLoopReached(INode n) {
    205208        return map.nodes.containsKey(n)
    206209                || (onewayMap.nodes.containsKey(n) && (onewayMap.nodes.get(n).size() > 1))
    207210                || ((firstCircular != null) && (firstCircular == n));
    public class RelationNodeMap {  
    209212
    210213    private Integer popBackwardOnewayPart(int way) {
    211214        if (lastOnewayNode != null) {
    212             Set<Node> nodes = new TreeSet<>();
     215            Set<INode> nodes = new TreeSet<>();
    213216            if (onewayReverseMap.ways.containsKey(way)) {
    214217                nodes.addAll(onewayReverseMap.ways.get(way));
    215218            }
    216219            if (map.ways.containsKey(way)) {
    217220                nodes.addAll(map.ways.get(way));
    218221            }
    219             for (Node n : nodes) {
     222            for (INode n : nodes) {
    220223                if (n == lastOnewayNode) { //if oneway part ends
    221224                    firstOneway = null;
    222225                    lastOnewayNode = null;
    public class RelationNodeMap {  
    247250     * @param n node
    248251     * @return node next to n
    249252     */
    250     private Integer deleteAndGetAdjacentNode(NodesWays nw, Node n) {
     253    private Integer deleteAndGetAdjacentNode(NodesWays nw, INode n) {
    251254        Integer j = findAdjacentWay(nw, n);
    252255        if (j == null) return null;
    253256        deleteWayNode(nw, j, n);
    254257        return j;
    255258    }
    256259
    257     private static Integer findAdjacentWay(NodesWays nw, Node n) {
     260    private static Integer findAdjacentWay(NodesWays nw, INode n) {
    258261        Set<Integer> adj = nw.nodes.get(n);
    259262        if (adj == null || adj.isEmpty()) return null;
    260263        return adj.iterator().next();
    261264    }
    262265
    263     private void deleteWayNode(NodesWays nw, Integer way, Node n) {
     266    private void deleteWayNode(NodesWays nw, Integer way, INode n) {
    264267        if (nw.oneWay) {
    265268            doneOneway(way);
    266269        } else {
    public class RelationNodeMap {  
    285288
    286289        if (remainingOneway.isEmpty()) return null;
    287290        for (Integer i : remainingOneway.keySet()) { //find oneway, which is connected to more than one way (is between two oneway loops)
    288             for (Node n : onewayReverseMap.ways.get(i)) {
     291            for (INode n : onewayReverseMap.ways.get(i)) {
    289292                if (onewayReverseMap.nodes.containsKey(n) && onewayReverseMap.nodes.get(n).size() > 1) {
    290293                    doneOneway(i);
    291294                    firstCircular = n;
    public class RelationNodeMap {  
    305308     * @param i member key
    306309     */
    307310    private void doneOneway(Integer i) {
    308         Set<Node> nodesForward = remainingOneway.get(i);
    309         for (Node n : nodesForward) {
     311        Set<INode> nodesForward = remainingOneway.get(i);
     312        for (INode n : nodesForward) {
    310313            if (onewayMap.nodes.containsKey(n)) {
    311314                onewayMap.nodes.get(n).remove(i);
    312315            }
    public class RelationNodeMap {  
    319322
    320323    private void done(Integer i) {
    321324        remaining.remove(i);
    322         Set<Node> nodes = map.ways.get(i);
    323         for (Node n : nodes) {
     325        Set<INode> nodes = map.ways.get(i);
     326        for (INode n : nodes) {
    324327            boolean result = map.nodes.get(n).remove(i);
    325328            if (!result) throw new AssertionError();
    326329        }
  • src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
    index d7457f7f1..70023011d 100644
    import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType  
    66import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction.ROUNDABOUT_RIGHT;
    77
    88import org.openstreetmap.josm.data.coor.EastNorth;
    9 import org.openstreetmap.josm.data.osm.Node;
    10 import org.openstreetmap.josm.data.osm.RelationMember;
    11 import org.openstreetmap.josm.data.osm.Way;
     9import org.openstreetmap.josm.data.osm.INode;
     10import org.openstreetmap.josm.data.osm.IRelationMember;
     11import org.openstreetmap.josm.data.osm.IWay;
    1212import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
    1313
    1414/**
    final class RelationSortUtils {  
    2424     * determine, if the way i is a roundabout and if yes, what type of roundabout
    2525     * @param member relation member
    2626     * @return roundabout type
     27     * @since xxx (generics)
    2728     */
    28     static Direction roundaboutType(RelationMember member) {
     29    static Direction roundaboutType(IRelationMember<?> member) {
    2930        if (member == null || !member.isWay()) return NONE;
    30         return roundaboutType(member.getWay());
     31        return roundaboutType((IWay<?>) member.getWay());
    3132    }
    3233
    33     static Direction roundaboutType(Way w) {
     34    /**
     35     * Check if a way is a roundabout type
     36     * @param w The way to check
     37     * @param <W> The way type
     38     * @return The roundabout type
     39     * @since xxx (generics)
     40     */
     41    static <W extends IWay<?>> Direction roundaboutType(W w) {
    3442        if (w != null && w.hasTag("junction", "circular", "roundabout")) {
    3543            int nodesCount = w.getNodesCount();
    3644            if (nodesCount > 2 && nodesCount < 200) {
    37                 Node n1 = w.getNode(0);
    38                 Node n2 = w.getNode(1);
    39                 Node n3 = w.getNode(2);
     45                INode n1 = w.getNode(0);
     46                INode n2 = w.getNode(1);
     47                INode n3 = w.getNode(2);
    4048                if (n1 != null && n2 != null && n3 != null && w.isClosed()) {
    4149                    /** do some simple determinant / cross product test on the first 3 nodes
    4250                        to see, if the roundabout goes clock wise or ccw */
    final class RelationSortUtils {  
    5462        return NONE;
    5563    }
    5664
    57     static boolean isBackward(final RelationMember member) {
     65    static boolean isBackward(final IRelationMember<?> member) {
    5866        return "backward".equals(member.getRole());
    5967    }
    6068
    61     static boolean isForward(final RelationMember member) {
     69    static boolean isForward(final IRelationMember<?> member) {
    6270        return "forward".equals(member.getRole());
    6371    }
    6472
    65     static boolean isOneway(final RelationMember member) {
     73    static boolean isOneway(final IRelationMember<?> member) {
    6674        return isForward(member) || isBackward(member);
    6775    }
    6876}
  • src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java

    diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
    index 12713094a..34d6bdf4f 100644
    import java.util.Objects;  
    1515import java.util.stream.Collectors;
    1616
    1717import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
     18import org.openstreetmap.josm.data.osm.IPrimitive;
     19import org.openstreetmap.josm.data.osm.IRelationMember;
    1820import org.openstreetmap.josm.data.osm.OsmPrimitive;
    1921import org.openstreetmap.josm.data.osm.Relation;
    2022import org.openstreetmap.josm.data.osm.RelationMember;
    public class RelationSorter {  
    194196     * Sorts a list of members by connectivity
    195197     * @param defaultMembers The members to sort
    196198     * @return A sorted list of the same members
     199     * @since xxx (signature change, generics)
    197200     */
    198     public static List<RelationMember> sortMembersByConnectivity(List<RelationMember> defaultMembers) {
     201    public static <T extends IRelationMember<? extends IPrimitive>> List<T> sortMembersByConnectivity(List<T> defaultMembers) {
     202        List<T> newMembers;
    199203
    200         List<RelationMember> newMembers;
    201 
    202         RelationNodeMap map = new RelationNodeMap(defaultMembers);
     204        RelationNodeMap<T> map = new RelationNodeMap<>(defaultMembers);
    203205        // List of groups of linked members
    204206        //
    205207        List<LinkedList<Integer>> allGroups = new ArrayList<>();
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    diff --git src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    index f00dd956f..049951fcd 100644
    import org.openstreetmap.josm.data.imagery.ImageryInfo;  
    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;
    import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChan  
    110111import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction;
    111112import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction;
    112113import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction;
     114import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
    113115import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
    114116import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction;
    115117import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    889891            if (coordinateConverter.requiresReprojection()) {
    890892                tile = new ReprojectionTile(tileSource, x, y, zoom);
    891893            } else {
    892                 tile = new Tile(tileSource, x, y, zoom);
     894                tile = createTile(tileSource, x, y, zoom);
    893895            }
    894896            tileCache.addTile(tile);
    895897        }
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    10421044                    img = getLoadedTileImage(tile);
    10431045                    anchorImage = getAnchor(tile, img);
    10441046                }
    1045                 if (img == null || anchorImage == null) {
     1047                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10461048                    miss = true;
    10471049                }
    10481050            }
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    10511053                return;
    10521054            }
    10531055
    1054             img = applyImageProcessors(img);
     1056            if (img != null) {
     1057                img = applyImageProcessors(img);
     1058            }
    10551059
    10561060            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10571061            synchronized (paintMutex) {
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    18631867
    18641868                for (int x = minX; x <= maxX; x++) {
    18651869                    for (int y = minY; y <= maxY; y++) {
    1866                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1870                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18671871                    }
    18681872                }
    18691873            }
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    19691973        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19701974    }
    19711975
     1976    /**
     1977     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     1978     *
     1979     * @param source Tile source
     1980     * @param x X coordinate
     1981     * @param y Y coordinate
     1982     * @param zoom Zoom level
     1983     * @return The new {@link Tile}
     1984     * @since xxx
     1985     */
     1986    public Tile createTile(T source, int x, int y, int zoom) {
     1987        return new Tile(source, x, y, zoom);
     1988    }
     1989
    19721990    @Override
    19731991    public synchronized void destroy() {
    19741992        super.destroy();
    implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi  
    19892007            allocateCacheMemory();
    19902008            if (memory != null) {
    19912009                doPaint(graphics);
     2010                if (AbstractTileSourceLayer.this instanceof MVTLayer) {
     2011                    AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView()
     2012                      .getRealBounds());
     2013                }
    19922014            } else {
    19932015                Graphics g = graphics.getDefaultGraphics();
    19942016                Color oldColor = g.getColor();
  • src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

    diff --git src/org/openstreetmap/josm/gui/layer/ImageryLayer.java src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
    index 6c902b92a..933933da3 100644
    import org.openstreetmap.josm.gui.MainApplication;  
    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;
    public abstract class ImageryLayer extends Layer {  
    168169        case BING:
    169170        case SCANEX:
    170171            return new TMSLayer(info);
     172        case MVT:
     173            return new MVTLayer(info);
    171174        default:
    172175            throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
    173176        }
  • new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java

    diff --git src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
    new file mode 100644
    index 000000000..aa335f7b0
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.Component;
     7import java.awt.Graphics2D;
     8import java.awt.event.ActionEvent;
     9import java.util.ArrayList;
     10import java.util.Arrays;
     11import java.util.Collection;
     12import java.util.Collections;
     13import java.util.HashMap;
     14import java.util.List;
     15import java.util.Map;
     16import java.util.Objects;
     17import java.util.function.BooleanSupplier;
     18import java.util.function.Consumer;
     19import java.util.stream.Collectors;
     20
     21import javax.swing.AbstractAction;
     22import javax.swing.Action;
     23import javax.swing.JCheckBoxMenuItem;
     24import javax.swing.JMenuItem;
     25
     26import org.openstreetmap.gui.jmapviewer.Tile;
     27import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     28import org.openstreetmap.josm.data.Bounds;
     29import org.openstreetmap.josm.data.imagery.ImageryInfo;
     30import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     31import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
     32import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     33import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener;
     34import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     35import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     36import org.openstreetmap.josm.data.osm.DataSet;
     37import org.openstreetmap.josm.data.osm.Node;
     38import org.openstreetmap.josm.data.osm.OsmPrimitive;
     39import org.openstreetmap.josm.data.osm.Relation;
     40import org.openstreetmap.josm.data.osm.RelationMember;
     41import org.openstreetmap.josm.data.osm.Way;
     42import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     43import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     44import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
     45import org.openstreetmap.josm.data.vector.VectorDataSet;
     46import org.openstreetmap.josm.data.vector.VectorNode;
     47import org.openstreetmap.josm.data.vector.VectorPrimitive;
     48import org.openstreetmap.josm.data.vector.VectorRelation;
     49import org.openstreetmap.josm.data.vector.VectorWay;
     50import org.openstreetmap.josm.gui.MainApplication;
     51import org.openstreetmap.josm.gui.MapView;
     52import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
     53import org.openstreetmap.josm.gui.layer.LayerManager;
     54import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     55import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     56import org.openstreetmap.josm.gui.mappaint.StyleSource;
     57
     58/**
     59 * A layer for MapBox Vector Tiles
     60 * @author Taylor Smock
     61 * @since xxx
     62 */
     63public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements TileListener {
     64    private static final String CACHE_REGION_NAME = "MVT";
     65    // Just to avoid allocating a bunch of 0 length action arrays
     66    private static final Action[] EMPTY_ACTIONS = new Action[0];
     67    private final Map<String, Boolean> layerNames = new HashMap<>();
     68    private final VectorDataSet dataSet = new VectorDataSet();
     69
     70    /**
     71     * Creates an instance of an MVT layer
     72     *
     73     * @param info ImageryInfo describing the layer
     74     */
     75    public MVTLayer(ImageryInfo info) {
     76        super(info);
     77    }
     78
     79    @Override
     80    protected Class<? extends TileLoader> getTileLoaderClass() {
     81        return MapBoxVectorCachedTileLoader.class;
     82    }
     83
     84    @Override
     85    protected String getCacheName() {
     86        return CACHE_REGION_NAME;
     87    }
     88
     89    @Override
     90    public Collection<String> getNativeProjections() {
     91        // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
     92        // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
     93        return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
     94    }
     95
     96    @Override
     97    public void paint(Graphics2D g, MapView mv, Bounds box) {
     98        this.dataSet.setZoom(this.getZoomLevel());
     99        AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, false);
     100        painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
     101          || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     102        // Set the painter to use our custom style sheet
     103        if (painter instanceof StyledMapRenderer && this.dataSet.getStyles() != null) {
     104            ((StyledMapRenderer) painter).setStyles(this.dataSet.getStyles());
     105        }
     106        painter.render(this.dataSet, false, box);
     107    }
     108
     109    @Override
     110    protected MapboxVectorTileSource getTileSource() {
     111        MapboxVectorTileSource source = new MapboxVectorTileSource(this.info);
     112        this.info.setAttribution(source);
     113        if (source.getStyleSource() != null) {
     114            List<ElemStyles> styles = source.getStyleSource().getSources().entrySet().stream()
     115              .filter(entry -> entry.getKey() == null || entry.getKey().getUrls().contains(source.getBaseUrl()))
     116              .map(Map.Entry::getValue).collect(Collectors.toList());
     117            // load the style sources
     118            styles.stream().map(ElemStyles::getStyleSources).flatMap(Collection::stream).forEach(StyleSource::loadStyleSource);
     119            this.dataSet.setStyles(styles);
     120            this.setName(source.getName());
     121        }
     122        return source;
     123    }
     124
     125    @Override
     126    public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
     127        final MVTTile tile = new MVTTile(source, x, y, zoom);
     128        tile.addTileLoaderFinisher(this);
     129        return tile;
     130    }
     131
     132    @Override
     133    public Action[] getMenuEntries() {
     134        ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries()));
     135        // Add separator between Info and the layers
     136        actions.add(SeparatorLayerAction.INSTANCE);
     137        for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
     138            actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
     139                    layer -> {
     140                layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
     141                this.invalidate();
     142            }));
     143        }
     144        // Add separator between layers and convert action
     145        actions.add(SeparatorLayerAction.INSTANCE);
     146        actions.add(new ConvertLayerAction(this));
     147        return actions.toArray(EMPTY_ACTIONS);
     148    }
     149
     150    /**
     151     * Get the data set for this layer
     152     */
     153    public VectorDataSet getData() {
     154        return this.dataSet;
     155    }
     156   
     157    private static class ConvertLayerAction extends AbstractAction implements LayerAction {
     158        private final MVTLayer layer;
     159
     160        ConvertLayerAction(MVTLayer layer) {
     161            this.layer = layer;
     162        }
     163
     164        @Override
     165        public void actionPerformed(ActionEvent e) {
     166            LayerManager manager = MainApplication.getLayerManager();
     167            VectorDataSet dataSet = layer.getData();
     168            DataSet osmData = new DataSet();
     169            // Add nodes first, map is to ensure we can map new nodes to vector nodes
     170            Map<VectorNode, Node> nodeMap = new HashMap<>(dataSet.getNodes().size());
     171            for (VectorNode vectorNode : dataSet.getNodes()) {
     172                Node newNode = new Node(vectorNode.getCoor());
     173                if (vectorNode.isTagged()) {
     174                    vectorNode.getInterestingTags().forEach(newNode::put);
     175                }
     176                nodeMap.put(vectorNode, newNode);
     177            }
     178            // Add ways next
     179            Map<VectorWay, Way> wayMap = new HashMap<>(dataSet.getWays().size());
     180            for (VectorWay vectorWay : dataSet.getWays()) {
     181                Way newWay = new Way();
     182                List<Node> nodes = vectorWay.getNodes().stream().map(nodeMap::get).filter(Objects::nonNull).collect(Collectors.toList());
     183                newWay.setNodes(nodes);
     184                if (vectorWay.isTagged()) {
     185                    vectorWay.getInterestingTags().forEach(newWay::put);
     186                }
     187                wayMap.put(vectorWay, newWay);
     188            }
     189
     190            // Finally, add Relations
     191            Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size());
     192            for (VectorRelation vectorRelation : dataSet.getRelations()) {
     193                Relation relation = new Relation();
     194                if (vectorRelation.isTagged()) {
     195                    vectorRelation.getInterestingTags().forEach(relation::put);
     196                }
     197                List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> {
     198                    final OsmPrimitive primitive;
     199                    final VectorPrimitive vectorPrimitive = member.getMember();
     200                    if (vectorPrimitive instanceof VectorNode) {
     201                        primitive = nodeMap.get(vectorPrimitive);
     202                    } else if (vectorPrimitive instanceof VectorWay) {
     203                        primitive = wayMap.get(vectorPrimitive);
     204                    } else if (vectorPrimitive instanceof VectorRelation) {
     205                        // Hopefully, relations are encountered in order...
     206                        primitive = relationMap.get(vectorPrimitive);
     207                    } else {
     208                        primitive = null;
     209                    }
     210                    if (primitive == null) return null;
     211                    return new RelationMember(member.getRole(), primitive);
     212                }).filter(Objects::nonNull).collect(Collectors.toList());
     213                relation.setMembers(members);
     214                relationMap.put(vectorRelation, relation);
     215            }
     216            try {
     217                osmData.beginUpdate();
     218                nodeMap.values().forEach(osmData::addPrimitive);
     219                wayMap.values().forEach(osmData::addPrimitive);
     220                relationMap.values().forEach(osmData::addPrimitive);
     221            } finally {
     222                osmData.endUpdate();
     223            }
     224            manager.addLayer(new OsmDataLayer(osmData, this.layer.getName(), null));
     225            manager.removeLayer(this.layer);
     226        }
     227
     228        @Override
     229        public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
     230            return layers.stream().allMatch(MVTLayer.class::isInstance);
     231        }
     232
     233        @Override
     234        public Component createMenuComponent() {
     235            JMenuItem menuItem = new JMenuItem(tr("Convert to OSM Data"));
     236            menuItem.addActionListener(this);
     237            return menuItem;
     238        }
     239    }
     240
     241    private static class EnableLayerAction extends AbstractAction implements LayerAction {
     242        private final String layer;
     243        private final Consumer<String> consumer;
     244        private final BooleanSupplier state;
     245
     246        EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) {
     247            super(tr("Toggle layer {0}", layer));
     248            this.layer = layer;
     249            this.consumer = consumer;
     250            this.state = state;
     251        }
     252
     253        @Override
     254        public void actionPerformed(ActionEvent e) {
     255            consumer.accept(layer);
     256        }
     257
     258        @Override
     259        public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
     260            return layers.stream().allMatch(MVTLayer.class::isInstance);
     261        }
     262
     263        @Override
     264        public Component createMenuComponent() {
     265            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
     266            item.setSelected(this.state.getAsBoolean());
     267            return item;
     268        }
     269    }
     270
     271    @Override
     272    public void finishedLoading(MVTTile tile) {
     273        for (Layer layer : tile.getLayers()) {
     274            this.layerNames.putIfAbsent(layer.getName(), true);
     275        }
     276        this.dataSet.addTileData(tile);
     277    }
     278}
  • src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java

    diff --git src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
    index fef095ea1..970635d96 100644
    public class ElemStyles implements PreferenceChangedListener {  
    8585        Config.getPref().addPreferenceChangeListener(this);
    8686    }
    8787
     88    /**
     89     * Constructs a new {@code ElemStyles} with specific style sources. This does not listen to preference changes,
     90     * and therefore should only be used with layers that have specific drawing requirements.
     91     *
     92     * @param sources The style sources (these cannot be added to, or removed from)
     93     * @since xxx
     94     */
     95    public ElemStyles(Collection<StyleSource> sources) {
     96        this.styleSources.addAll(sources);
     97    }
     98
    8899    /**
    89100     * Clear the style cache for all primitives of all DataSets.
    90101     */
    public class ElemStyles implements PreferenceChangedListener {  
    151162     * @since 13810 (signature)
    152163     */
    153164    public Pair<StyleElementList, Range> getStyleCacheWithRange(IPrimitive osm, double scale, NavigatableComponent nc) {
    154         if (!osm.isCachedStyleUpToDate() || scale <= 0) {
    155             osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE);
    156         } else {
    157             Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected());
    158             if (lst.a != null)
    159                 return lst;
    160         }
    161         Pair<StyleElementList, Range> p = getImpl(osm, scale, nc);
    162         if (osm instanceof INode && isDefaultNodes()) {
    163             if (p.a.isEmpty()) {
    164                 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
    165                     p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT;
    166                 } else {
    167                     p.a = DefaultStyles.DEFAULT_NODE_STYLELIST;
    168                 }
     165        synchronized (osm.getStyleCacheSyncObject()) {
     166            if (!osm.isCachedStyleUpToDate() || scale <= 0) {
     167                osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE);
    169168            } else {
    170                 boolean hasNonModifier = false;
    171                 boolean hasText = false;
    172                 for (StyleElement s : p.a) {
    173                     if (s instanceof BoxTextElement) {
    174                         hasText = true;
     169                Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected());
     170                if (lst.a != null)
     171                    return lst;
     172            }
     173            Pair<StyleElementList, Range> p = getImpl(osm, scale, nc);
     174            if (osm instanceof INode && isDefaultNodes()) {
     175                if (p.a.isEmpty()) {
     176                    if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
     177                        p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT;
    175178                    } else {
    176                         if (!s.isModifier) {
    177                             hasNonModifier = true;
     179                        p.a = DefaultStyles.DEFAULT_NODE_STYLELIST;
     180                    }
     181                } else {
     182                    boolean hasNonModifier = false;
     183                    boolean hasText = false;
     184                    for (StyleElement s : p.a) {
     185                        if (s instanceof BoxTextElement) {
     186                            hasText = true;
     187                        } else {
     188                            if (!s.isModifier) {
     189                                hasNonModifier = true;
     190                            }
    178191                        }
    179192                    }
    180                 }
    181                 if (!hasNonModifier) {
    182                     p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE);
    183                     if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
    184                         p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE);
     193                    if (!hasNonModifier) {
     194                        p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE);
     195                        if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
     196                            p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE);
     197                        }
    185198                    }
    186199                }
    187             }
    188         } else if (osm instanceof IWay && isDefaultLines()) {
    189             boolean hasProperLineStyle = false;
    190             for (StyleElement s : p.a) {
    191                 if (s.isProperLineStyle()) {
    192                     hasProperLineStyle = true;
    193                     break;
    194                 }
    195             }
    196             if (!hasProperLineStyle) {
    197                 LineElement line = LineElement.UNTAGGED_WAY;
    198                 for (StyleElement element : p.a) {
    199                     if (element instanceof AreaElement) {
    200                         line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true);
     200            } else if (osm instanceof IWay && isDefaultLines()) {
     201                boolean hasProperLineStyle = false;
     202                for (StyleElement s : p.a) {
     203                    if (s.isProperLineStyle()) {
     204                        hasProperLineStyle = true;
    201205                        break;
    202206                    }
    203207                }
    204                 p.a = new StyleElementList(p.a, line);
     208                if (!hasProperLineStyle) {
     209                    LineElement line = LineElement.UNTAGGED_WAY;
     210                    for (StyleElement element : p.a) {
     211                        if (element instanceof AreaElement) {
     212                            line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true);
     213                            break;
     214                        }
     215                    }
     216                    p.a = new StyleElementList(p.a, line);
     217                }
    205218            }
     219            StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE;
     220            try {
     221                osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected()));
     222            } catch (RangeViolatedError e) {
     223                throw new AssertionError("Range violated: " + e.getMessage()
     224                  + " (object: " + osm.getPrimitiveId() + ", current style: " + osm.getCachedStyle()
     225                  + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
     226            }
     227            osm.declareCachedStyleUpToDate();
     228            return p;
    206229        }
    207         StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE;
    208         try {
    209             osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected()));
    210         } catch (RangeViolatedError e) {
    211             throw new AssertionError("Range violated: " + e.getMessage()
    212                     + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.getCachedStyle()
    213                     + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
    214         }
    215         osm.declareCachedStyleUpToDate();
    216         return p;
    217230    }
    218231
    219232    /**
    public class ElemStyles implements PreferenceChangedListener {  
    376389     * @since 13810 (signature)
    377390     */
    378391    public Pair<StyleElementList, Range> generateStyles(IPrimitive osm, double scale, boolean pretendWayIsClosed) {
    379 
    380392        List<StyleElement> sl = new ArrayList<>();
    381393        MultiCascade mc = new MultiCascade();
    382394        Environment env = new Environment(osm, mc, null, null);
  • src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java

    diff --git src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
    index f1e05a5f9..a19916e5b 100644
    public final class ConditionFactory {  
    875875            }
    876876            return e.osm.isSelected();
    877877        }
     878
     879        /**
     880         * Check if the object is highlighted (i.e., is hovered over)
     881         * @param e The MapCSS environment
     882         * @return {@code true} if the object is highlighted
     883         * @see IPrimitive#isHighlighted
     884         * @since xxx
     885         */
     886        static boolean highlighted(Environment e) { // NO_UCD (unused code)
     887            return e.osm.isHighlighted();
     888        }
    878889    }
    879890
    880891    /**
  • new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java

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

    diff --git src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
    index d1d0ed096..cb78f9bda 100644
    public class ImageryProvidersPanel extends JPanel {  
    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));
    public class ImageryProvidersPanel extends JPanel {  
    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            }
    public class ImageryProvidersPanel extends JPanel {  
    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            }
    public class ImageryProvidersPanel extends JPanel {  
    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);
    public class ImageryProvidersPanel extends JPanel {  
    749756                Logging.trace(e1);
    750757                // give a second chance with a default Locale 'en'
    751758                try {
    752                     url = new URL(eulaUrl.replaceAll("\\{lang\\}", ""));
     759                    url = new URL(eulaUrl.replaceAll("\\{lang}", ""));
    753760                    htmlPane = new JosmEditorPane(url);
    754761                } catch (IOException e2) {
    755762                    Logging.debug(e2);
  • new file test/data/mapillary.json

    diff --git test/data/mapillary.json test/data/mapillary.json
    new file mode 100644
    index 000000000..0f6f9483d
    - +  
     1{
     2  "version":8,
     3  "name":"Mapillary",
     4  "owner":"Mapillary",
     5  "id":"mapillary",
     6  "sources":{
     7      "mapillary-source":{
     8        "type":"vector",
     9        "tiles":[
     10            "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt"
     11        ],
     12        "maxzoom":14
     13      },
     14      "mapillary-features-source": {
     15        "maxzoom": 20,
     16        "minzoom": 14,
     17        "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=points&per_page=1000" ],
     18        "type": "vector"
     19      },
     20      "mapillary-traffic-signs-source": {
     21        "maxzoom": 20,
     22        "minzoom": 14,
     23        "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=trafficsigns&per_page=1000" ],
     24        "type": "vector"
     25      }
     26  },
     27  "layers":[
     28    {
     29      "filter": [ "==", "pano", 1 ],
     30      "id": "mapillary-panos",
     31      "type": "circle",
     32      "source": "mapillary-source",
     33      "source-layer": "mapillary-images",
     34      "minzoom": 17,
     35      "paint": {
     36        "circle-color": "#05CB63",
     37        "circle-opacity": 0.5,
     38        "circle-radius": 18
     39      }
     40    },
     41    {
     42      "id": "mapillary-dots",
     43      "type": "circle",
     44      "source": "mapillary-source",
     45      "source-layer": "mapillary-images",
     46      "interactive": true,
     47      "minzoom": 14,
     48      "paint": {
     49        "circle-color": "#05CB63",
     50        "circle-radius": 6
     51      }
     52    },
     53    {
     54      "id": "mapillary-lines",
     55      "type": "line",
     56      "source": "mapillary-source",
     57      "source-layer": "mapillary-sequences",
     58      "minzoom": 6,
     59      "paint": {
     60        "line-color": "#05CB63",
     61        "line-width": 2
     62      }
     63    },
     64    {
     65      "id": "mapillary-overview",
     66      "type": "circle",
     67      "source": "mapillary-source",
     68      "source-layer": "mapillary-sequence-overview",
     69      "maxzoom": 6,
     70      "paint": {
     71        "circle-radius": 4,
     72        "circle-opacity": 0.6,
     73        "circle-color": "#05CB63"
     74      }
     75    },
     76    {
     77      "id": "mapillary-features",
     78      "type": "symbol",
     79      "source": "mapillary-features-source",
     80      "source-layer": "mapillary-map-features",
     81      "interactive": true,
     82      "minzoom": 14,
     83      "layout": {
     84        "icon-image": "{value}",
     85        "icon-allow-overlap": true,
     86        "symbol-avoid-edges": true
     87      },
     88      "paint": {
     89        "text-color": "#fff",
     90        "text-halo-color": "#000"
     91      }
     92    },
     93    {
     94      "id": "mapillary-traffic-signs",
     95      "type": "symbol",
     96      "source": "mapillary-traffic-signs-source",
     97      "source-layer": "mapillary-map-features",
     98      "interactive": true,
     99      "minzoom": 14,
     100      "layout": {
     101        "icon-image": "{value}",
     102        "icon-allow-overlap": true,
     103        "symbol-avoid-edges": true
     104      },
     105      "paint": {
     106        "text-color": "#fff",
     107        "text-halo-color": "#000"
     108      }
     109    }
     110  ]
     111}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java

    diff --git test/data/pbf/mapillary/14/3248/6258.mvt test/data/pbf/mapillary/14/3248/6258.mvt
    new file mode 100644
    index 000000000..ff6a462a3
    Binary files /dev/null and test/data/pbf/mapillary/14/3248/6258.mvt differ
    diff --git test/data/pbf/mapillary/14/3249/6258.mvt test/data/pbf/mapillary/14/3249/6258.mvt
    new file mode 100644
    index 000000000..c5278577e
    Binary files /dev/null and test/data/pbf/mapillary/14/3249/6258.mvt differ
    diff --git test/data/pbf/openinframap/17/26028/50060.pbf test/data/pbf/openinframap/17/26028/50060.pbf
    new file mode 100644
    index 000000000..358270e1b
    Binary files /dev/null and test/data/pbf/openinframap/17/26028/50060.pbf differ
    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java
    new file mode 100644
    index 000000000..5468fe649
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertNotNull;
     7import static org.junit.jupiter.api.Assertions.assertSame;
     8import static org.junit.jupiter.api.Assertions.assertTrue;
     9
     10
     11import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getSimpleFeatureLayerBytes;
     12import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getLayer;
     13
     14import java.text.NumberFormat;
     15import java.util.Arrays;
     16
     17import org.junit.jupiter.api.Test;
     18
     19/**
     20 * Test class for {@link Feature}
     21 */
     22class FeatureTest {
     23    /**
     24     * This can be used to replace bytes 11-14 (inclusive) in {@link LayerTest#simpleFeatureLayerBytes}.
     25     */
     26    private final byte[] nonPackedTags = new byte[] {0x10, 0x00, 0x10, 0x00};
     27
     28    @Test
     29    void testCreation() {
     30        testCreation(getSimpleFeatureLayerBytes());
     31    }
     32
     33    @Test
     34    void testCreationUnpacked() {
     35        byte[] copyBytes = getSimpleFeatureLayerBytes();
     36        System.arraycopy(nonPackedTags, 0, copyBytes, 13, nonPackedTags.length);
     37        testCreation(copyBytes);
     38    }
     39
     40    @Test
     41    void testCreationTrueToFalse() {
     42        byte[] copyBytes = getSimpleFeatureLayerBytes();
     43        copyBytes[copyBytes.length - 1] = 0x00; // set value=false
     44        Layer layer = assertDoesNotThrow(() -> getLayer(copyBytes));
     45        assertSame(Boolean.FALSE, layer.getValue(0));
     46    }
     47
     48    @Test
     49    void testNumberGrouping() {
     50        // This is the float we are adding
     51        // 49 74 24 00 == 1_000_000f
     52        // 3f 80 00 00 == 1f
     53        byte[] newBytes = new byte[] {0x22, 0x09, 0x15, 0x00, 0x24, 0x74, 0x49};
     54        byte[] copyBytes = Arrays.copyOf(getSimpleFeatureLayerBytes(), getSimpleFeatureLayerBytes().length + newBytes.length - 4);
     55        // Change last few bytes
     56        System.arraycopy(newBytes, 0, copyBytes, 25, newBytes.length);
     57        // Update the length of the record
     58        copyBytes[1] = (byte) (copyBytes[1] + newBytes.length - 4);
     59        final NumberFormat numberFormat = NumberFormat.getNumberInstance();
     60        final boolean numberFormatGroupingUsed = numberFormat.isGroupingUsed();
     61        // Sanity check
     62        Layer layer;
     63        try {
     64            numberFormat.setGroupingUsed(true);
     65            layer = assertDoesNotThrow(() -> getLayer(copyBytes));
     66            assertTrue(numberFormat.isGroupingUsed());
     67        } finally {
     68            numberFormat.setGroupingUsed(numberFormatGroupingUsed);
     69        }
     70        assertEquals(1, layer.getFeatures().size());
     71        assertEquals("t", layer.getName());
     72        assertEquals(2, layer.getVersion());
     73        assertEquals("a", layer.getKey(0));
     74        assertEquals(1_000_000f, ((Number) layer.getValue(0)).floatValue(), 0.00001);
     75       
     76        // Feature check
     77        Feature feature = layer.getFeatures().iterator().next();
     78        checkDefaultGeometry(feature);
     79        assertEquals("1000000", feature.getTags().get("a"));
     80    }
     81
     82    private void testCreation(byte[] bytes) {
     83        Layer layer = assertDoesNotThrow(() -> getLayer(bytes));
     84        // Sanity check the layer
     85        assertEquals(1, layer.getFeatures().size());
     86        assertEquals("t", layer.getName());
     87        assertEquals(2, layer.getVersion());
     88        assertEquals("a", layer.getKey(0));
     89        assertSame(Boolean.TRUE, layer.getValue(0));
     90
     91        // OK. Get the feature.
     92        Feature feature = layer.getFeatures().iterator().next();
     93
     94        checkDefaultTags(feature);
     95
     96        // Check id (should be the default of 0)
     97        assertEquals(1, feature.getId());
     98
     99        checkDefaultGeometry(feature);
     100    }
     101
     102    private void checkDefaultTags(Feature feature) {
     103        // Check tags
     104        assertEquals(1, feature.getTags().size());
     105        assertTrue(feature.getTags().containsKey("a"));
     106        // We are converting to a tag map (Map<String, String>), so "true"
     107        assertEquals("true", feature.getTags().get("a"));
     108    }
     109
     110    private void checkDefaultGeometry(Feature feature) {
     111        // Check the geometry
     112        assertEquals(GeometryTypes.POINT, feature.getGeometryType());
     113        assertEquals(1, feature.getGeometry().size());
     114        CommandInteger geometry = feature.getGeometry().get(0);
     115        assertEquals(Command.MoveTo, geometry.getType());
     116        assertEquals(2, geometry.getOperations().length);
     117        assertEquals(25, geometry.getOperations()[0]);
     118        assertEquals(17, geometry.getOperations()[1]);
     119        assertNotNull(feature.getGeometryObject());
     120        assertEquals(feature.getGeometryObject(), feature.getGeometryObject());
     121    }
     122}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
    new file mode 100644
    index 000000000..175d64cd7
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertFalse;
     6import static org.junit.jupiter.api.Assertions.assertThrows;
     7import static org.junit.jupiter.api.Assertions.assertTrue;
     8
     9
     10import java.awt.geom.Area;
     11import java.awt.geom.Ellipse2D;
     12import java.awt.geom.Path2D;
     13import java.awt.geom.PathIterator;
     14import java.awt.geom.Point2D;
     15import java.util.ArrayList;
     16import java.util.Arrays;
     17import java.util.Collections;
     18import java.util.List;
     19
     20import org.junit.jupiter.api.Test;
     21
     22/**
     23 * Test class for {@link Geometry}
     24 * @author Taylor Smock
     25 * @since xxx
     26 */
     27class GeometryTest {
     28    /**
     29     * Create a command integer fairly easily
     30     * @param command The command type (see {@link Command})
     31     * @param parameters The parameters for the command
     32     * @return A command integer
     33     */
     34    private static CommandInteger createCommandInteger(int command, int... parameters) {
     35        CommandInteger commandInteger = new CommandInteger(command);
     36        if (parameters != null) {
     37            for (int parameter : parameters) {
     38                commandInteger.addParameter(parameter);
     39            }
     40        }
     41        return commandInteger;
     42    }
     43
     44    /**
     45     * Check the current
     46     * @param pathIterator The path to check
     47     * @param expected The expected coords
     48     */
     49    private static void checkCurrentSegmentAndIncrement(PathIterator pathIterator, float... expected) {
     50        float[] coords = new float[6];
     51        int type = pathIterator.currentSegment(coords);
     52        pathIterator.next();
     53        for (int i = 0; i < expected.length; i++) {
     54            assertEquals(expected[i], coords[i]);
     55        }
     56        if (Arrays.asList(PathIterator.SEG_MOVETO, PathIterator.SEG_LINETO).contains(type)) {
     57            assertEquals(2, expected.length, "You should check both x and y coordinates");
     58        } else if (PathIterator.SEG_QUADTO == type) {
     59            assertEquals(4, expected.length, "You should check all x and y coordinates");
     60        } else if (PathIterator.SEG_CUBICTO == type) {
     61            assertEquals(6, expected.length, "You should check all x and y coordinates");
     62        } else if (PathIterator.SEG_CLOSE == type) {
     63            assertEquals(0, expected.length, "CloseTo has no expected coordinates to check");
     64        }
     65    }
     66
     67    @Test
     68    void testBadGeometry() {
     69        IllegalArgumentException badPointException = assertThrows(IllegalArgumentException.class,
     70          () -> new Geometry(GeometryTypes.POINT, Collections.singletonList(createCommandInteger(1))));
     71        assertEquals("POINT with 0 arguments is not understood", badPointException.getMessage());
     72        IllegalArgumentException badLineException = assertThrows(IllegalArgumentException.class,
     73          () -> new Geometry(GeometryTypes.LINESTRING, Collections.singletonList(createCommandInteger(15))));
     74        assertEquals("LINESTRING with 0 arguments is not understood", badLineException.getMessage());
     75    }
     76
     77    @Test
     78    void testPoint() {
     79        CommandInteger moveTo = createCommandInteger(9, 17, 34);
     80        Geometry geometry = new Geometry(GeometryTypes.POINT, Collections.singletonList(moveTo));
     81        assertEquals(1, geometry.getShapes().size());
     82        Ellipse2D shape = (Ellipse2D) geometry.getShapes().iterator().next();
     83        assertEquals(17, shape.getCenterX());
     84        assertEquals(34, shape.getCenterY());
     85    }
     86
     87    @Test
     88    void testLine() {
     89        CommandInteger moveTo = createCommandInteger(9, 2, 2);
     90        CommandInteger lineTo = createCommandInteger(18, 0, 8, 8, 0);
     91        Geometry geometry = new Geometry(GeometryTypes.LINESTRING, Arrays.asList(moveTo, lineTo));
     92        assertEquals(1, geometry.getShapes().size());
     93        Path2D path = (Path2D) geometry.getShapes().iterator().next();
     94        PathIterator pathIterator = path.getPathIterator(null);
     95        checkCurrentSegmentAndIncrement(pathIterator, 2, 2);
     96        checkCurrentSegmentAndIncrement(pathIterator, 2, 10);
     97        checkCurrentSegmentAndIncrement(pathIterator, 10, 10);
     98        assertTrue(pathIterator.isDone());
     99    }
     100
     101    @Test
     102    void testPolygon() {
     103        List<CommandInteger> commands = new ArrayList<>(3);
     104        commands.add(createCommandInteger(9, 3, 6));
     105        commands.add(createCommandInteger(18, 5, 6, 12, 22));
     106        commands.add(createCommandInteger(15));
     107
     108        Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands);
     109        assertEquals(1, geometry.getShapes().size());
     110
     111        Area area = (Area) geometry.getShapes().iterator().next();
     112        PathIterator pathIterator = area.getPathIterator(null);
     113        checkCurrentSegmentAndIncrement(pathIterator, 3, 6);
     114        // This is somewhat unexpected, and may change based off of JVM implementations
     115        // But for whatever reason, Java flips the inner coordinates in this case.
     116        checkCurrentSegmentAndIncrement(pathIterator, 20, 34);
     117        checkCurrentSegmentAndIncrement(pathIterator, 8, 12);
     118        checkCurrentSegmentAndIncrement(pathIterator, 3, 6);
     119        checkCurrentSegmentAndIncrement(pathIterator);
     120        assertTrue(pathIterator.isDone());
     121    }
     122
     123    @Test
     124    void testBadPolygon() {
     125        /*
     126         * "Linear rings MUST be geometric objects that have no anomalous geometric points,
     127         * such as self-intersection or self-tangency. The position of the cursor before
     128         * calling the ClosePath command of a linear ring SHALL NOT repeat the same position
     129         * as the first point in the linear ring as this would create a zero-length line
     130         * segment. A linear ring SHOULD NOT have an area calculated by the surveyor's
     131         * formula equal to zero, as this would signify a ring with anomalous geometric points."
     132         */
     133        List<CommandInteger> commands = new ArrayList<>(3);
     134        commands.add(createCommandInteger(9, 0, 0));
     135        commands.add(createCommandInteger(18, 0, 0));
     136        commands.add(createCommandInteger(15));
     137        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Geometry(GeometryTypes.POLYGON, commands));
     138        assertEquals("POLYGON cannot have zero area", exception.getMessage());
     139    }
     140
     141    @Test
     142    void testMultiPolygon() {
     143        List<CommandInteger> commands = new ArrayList<>(10);
     144        // Polygon 1
     145        commands.add(createCommandInteger(9, 0, 0));
     146        commands.add(createCommandInteger(26, 10, 0, 0, 10, -10, 0));
     147        commands.add(createCommandInteger(15));
     148        // Polygon 2 outer
     149        commands.add(createCommandInteger(9, 11, 1));
     150        commands.add(createCommandInteger(26, 9, 0, 0, 9, -9, 0));
     151        commands.add(createCommandInteger(15));
     152        // Polygon 2 inner
     153        commands.add(createCommandInteger(9, 2, -7));
     154        commands.add(createCommandInteger(26, 0, 4, 4, 0, 0, -4));
     155        commands.add(createCommandInteger(15));
     156
     157        Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands);
     158        assertEquals(1, geometry.getShapes().size());
     159        Area area = (Area) geometry.getShapes().iterator().next();
     160        assertFalse(area.isSingular());
     161        PathIterator pathIterator = area.getPathIterator(null);
     162        assertEquals(PathIterator.WIND_NON_ZERO, pathIterator.getWindingRule());
     163        assertTrue(area.contains(new Point2D.Float(5, 5)));
     164        assertTrue(area.contains(new Point2D.Float(12, 12)));
     165        assertFalse(area.contains(new Point2D.Float(15, 15)));
     166        assertFalse(area.contains(new Point2D.Float(10, 11)));
     167        assertFalse(area.contains(new Point2D.Float(-1, -1)));
     168    }
     169}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java
    new file mode 100644
    index 000000000..da0f9b3c7
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.fail;
     6
     7
     8import org.openstreetmap.josm.TestUtils;
     9
     10import org.junit.jupiter.api.Test;
     11import org.junit.jupiter.params.ParameterizedTest;
     12import org.junit.jupiter.params.provider.EnumSource;
     13
     14/**
     15 * Test class for {@link GeometryTypes}
     16 * @author Taylor Smock
     17 * @since xxx
     18 */
     19class GeometryTypesTest {
     20    @Test
     21    void testNaiveEnumTest() {
     22        TestUtils.superficialEnumCodeCoverage(GeometryTypes.class);
     23        TestUtils.superficialEnumCodeCoverage(GeometryTypes.Ring.class);
     24    }
     25
     26    @ParameterizedTest
     27    @EnumSource(GeometryTypes.class)
     28    void testExpectedIds(GeometryTypes type) {
     29        // Ensure that users can get the type from the ordinal
     30        // See https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L8
     31        // for the expected values
     32        final int expectedId;
     33        if (type == GeometryTypes.UNKNOWN) {
     34            expectedId = 0;
     35        } else if (type == GeometryTypes.POINT) {
     36            expectedId = 1;
     37        } else if (type == GeometryTypes.LINESTRING) {
     38            expectedId = 2;
     39        } else if (type == GeometryTypes.POLYGON) {
     40            expectedId = 3;
     41        } else {
     42            fail("Unknown geometry type, see vector tile spec");
     43            expectedId = Integer.MIN_VALUE;
     44        }
     45        assertEquals(expectedId, type.ordinal());
     46    }
     47}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
    new file mode 100644
    index 000000000..fc3ba9c27
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertThrows;
     7
     8import java.io.FileInputStream;
     9import java.io.IOException;
     10import java.util.Arrays;
     11import java.util.List;
     12
     13import org.openstreetmap.josm.TestUtils;
     14import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     15import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     16
     17import nl.jqno.equalsverifier.EqualsVerifier;
     18import org.junit.jupiter.api.Test;
     19
     20/**
     21 * Test class for {@link Layer}
     22 */
     23public class LayerTest {
     24    /**
     25     * This looks something like this (if it were json). Note that some keys could be repeated,
     26     * and so could be better represented as an array. Specifically, "features", "key", and "value".
     27     * "layer": {
     28     *     "name": "t",
     29     *     "version": 2,
     30     *     "features": {
     31     *         "type": "POINT",
     32     *         "tags": [0, 0],
     33     *         "geometry": [9, 50, 34]
     34     *     },
     35     *     "key": "a",
     36     *     "value": true
     37     * }
     38     *
     39     * WARNING: DO NOT MODIFY THIS ARRAY DIRECTLY -- it could contaminate other tests
     40     */
     41    private static final byte[] simpleFeatureLayerBytes = new byte[] {
     42      0x1a, 0x1b, // layer, 27 bytes for the rest
     43      0x0a, 0x01, 0x74, // name=t
     44      0x78, 0x02, // version=2
     45      0x12, 0x0d, // features, 11 bytes
     46      0x08, 0x01, // id=1
     47      0x18, 0x01, // type=POINT
     48      0x12, 0x02, 0x00, 0x00, // tags=[0, 0] (packed). Non-packed would be [0x10, 0x00, 0x10, 0x00]
     49      0x22, 0x03, 0x09, 0x32, 0x22, // geometry=[9, 50, 34]
     50      0x1a, 0x01, 0x61, // key=a
     51      0x22, 0x02, 0x38, 0x01, // value=true (boolean)
     52    };
     53
     54    /**
     55     * Gets a copy of {@link #simpleFeatureLayerBytes} so that a test doesn't accidentally change the bytes
     56     * @return An array that can be modified.
     57     */
     58    static byte[] getSimpleFeatureLayerBytes() {
     59        return Arrays.copyOf(simpleFeatureLayerBytes, simpleFeatureLayerBytes.length);
     60    }
     61
     62    /**
     63     * Create a layer from bytes
     64     * @param bytes The bytes that make up the layer
     65     * @return The generated layer
     66     * @throws IOException If something happened (should never trigger)
     67     */
     68    static Layer getLayer(byte[] bytes) throws IOException {
     69        List<ProtoBufRecord> records = (List<ProtoBufRecord>) new ProtoBufParser(bytes).allRecords();
     70        assertEquals(1, records.size());
     71        return new Layer(new ProtoBufParser(records.get(0).getBytes()).allRecords());
     72    }
     73
     74    @Test
     75    void testLayerCreation() throws IOException {
     76        List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
     77          + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
     78        Layer sequenceLayer = new Layer(layers.get(0).getBytes());
     79        assertEquals("mapillary-sequences", sequenceLayer.getName());
     80        assertEquals(1, sequenceLayer.getFeatures().size());
     81        assertEquals(1, sequenceLayer.getGeometry().size());
     82        assertEquals(2048, sequenceLayer.getExtent());
     83        assertEquals(1, sequenceLayer.getVersion());
     84
     85        Layer imageLayer = new Layer(layers.get(1).getBytes());
     86        assertEquals("mapillary-images", imageLayer.getName());
     87        assertEquals(116, imageLayer.getFeatures().size());
     88        assertEquals(116, imageLayer.getGeometry().size());
     89        assertEquals(2048, imageLayer.getExtent());
     90        assertEquals(1, imageLayer.getVersion());
     91    }
     92
     93    @Test
     94    void testLayerEqualsHashCode() throws IOException {
     95        List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
     96          + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
     97        EqualsVerifier.forClass(Layer.class).withPrefabValues(byte[].class, layers.get(0).getBytes(), layers.get(1).getBytes())
     98          .verify();
     99    }
     100
     101    @Test
     102    void testVersionsNumbers() {
     103        byte[] copyByte = getSimpleFeatureLayerBytes();
     104        assertEquals(2, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
     105        copyByte[6] = 1;
     106        assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
     107        copyByte[6] = 0;
     108        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
     109        assertEquals("We do not understand version 0 of the vector tile specification", exception.getMessage());
     110        copyByte[6] = 3;
     111        exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
     112        assertEquals("We do not understand version 3 of the vector tile specification", exception.getMessage());
     113        // Remove version number (AKA change it to some unknown field). Default is version=1.
     114        copyByte[5] = 0x18;
     115        assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
     116    }
     117
     118    @Test
     119    void testLayerName() throws IOException {
     120        byte[] copyByte = getSimpleFeatureLayerBytes();
     121        Layer layer = getLayer(copyByte);
     122        assertEquals("t", layer.getName());
     123        copyByte[2] = 0x1a; // name=t -> ?
     124        Exception noNameException = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
     125        assertEquals("Vector tile layers must have a layer name", noNameException.getMessage());
     126    }
     127
     128    @Test
     129    void testUnknownField() {
     130        byte[] copyByte = getSimpleFeatureLayerBytes();
     131        copyByte[27] = 0x78;
     132        Exception unknownField = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
     133        assertEquals("Unknown field in vector tile layer value (15)", unknownField.getMessage());
     134    }
     135}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
    new file mode 100644
    index 000000000..66e4ea781
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNull;
     6
     7import java.awt.image.BufferedImage;
     8import java.util.Collections;
     9import java.util.stream.Stream;
     10
     11import org.openstreetmap.gui.jmapviewer.Tile;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     13import org.openstreetmap.josm.TestUtils;
     14import org.openstreetmap.josm.data.cache.JCSCacheManager;
     15import org.openstreetmap.josm.data.imagery.ImageryInfo;
     16import org.openstreetmap.josm.data.imagery.TileJobOptions;
     17import org.openstreetmap.josm.testutils.JOSMTestRules;
     18
     19import org.awaitility.Awaitility;
     20import org.awaitility.Durations;
     21import org.junit.jupiter.api.BeforeEach;
     22import org.junit.jupiter.api.extension.RegisterExtension;
     23import org.junit.jupiter.params.ParameterizedTest;
     24import org.junit.jupiter.params.provider.Arguments;
     25import org.junit.jupiter.params.provider.MethodSource;
     26
     27/**
     28 * Test class for {@link MVTTile}
     29 */
     30public class MVTTileTest {
     31    private MapboxVectorTileSource tileSource;
     32    private MapBoxVectorCachedTileLoader loader;
     33    @RegisterExtension
     34    JOSMTestRules rule = new JOSMTestRules();
     35    @BeforeEach
     36    void setup() {
     37        tileSource = new MapboxVectorTileSource(new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot()
     38          + "pbf/mapillary/{z}/{x}/{y}.mvt"));
     39        loader = new MapBoxVectorCachedTileLoader(null,
     40          JCSCacheManager.getCache("testMapillaryCache"), new TileJobOptions(1, 1, Collections
     41          .emptyMap(), 3600));
     42    }
     43
     44    /**
     45     * Provide arguments for {@link #testMVTTile(BufferedImage, Boolean)}
     46     * @return The arguments to use
     47     */
     48    private static Stream<Arguments> testMVTTile() {
     49        return Stream.of(
     50          Arguments.of(null, Boolean.TRUE),
     51          Arguments.of(Tile.LOADING_IMAGE, Boolean.TRUE),
     52          Arguments.of(Tile.ERROR_IMAGE, Boolean.TRUE),
     53          Arguments.of(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), Boolean.FALSE)
     54        );
     55    }
     56
     57    @ParameterizedTest
     58    @MethodSource("testMVTTile")
     59    void testMVTTile(BufferedImage image, Boolean isLoaded) {
     60        MVTTile tile = new MVTTile(tileSource, 3249, 6258, 14);
     61        tile.setImage(image);
     62        assertEquals(image, tile.getImage());
     63
     64        TileJob job = loader.createTileLoaderJob(tile);
     65        job.submit();
     66        Awaitility.await().atMost(Durations.ONE_SECOND).until(tile::isLoaded);
     67        if (isLoaded) {
     68            Awaitility.await().atMost(Durations.ONE_SECOND).until(() -> tile.getLayers() != null && tile.getLayers().size() > 1);
     69            assertEquals(2, tile.getLayers().size());
     70            // The test Mapillary tiles have 2048 instead of 4096 for their extent. This *may* change
     71            // in future Mapillary tiles, so if the test PBF files are updated, beware.
     72            assertEquals(2048, tile.getExtent());
     73            // Ensure that we have the clear image set, such that the tile doesn't add to the dataset again
     74            // and we don't have a loading image
     75            assertEquals(MVTTile.CLEAR_LOADED, tile.getImage());
     76        } else {
     77            assertNull(tile.getLayers());
     78            assertEquals(image, tile.getImage());
     79        }
     80    }
     81
     82}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
    new file mode 100644
    index 000000000..5b9f16842
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.assertNull;
     7
     8
     9import java.util.stream.Stream;
     10
     11import org.junit.jupiter.api.extension.RegisterExtension;
     12import org.openstreetmap.josm.TestUtils;
     13import org.openstreetmap.josm.data.imagery.ImageryInfo;
     14import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
     15import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
     16import org.openstreetmap.josm.gui.ExtendedDialog;
     17import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     18import org.openstreetmap.josm.testutils.JOSMTestRules;
     19import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
     20
     21import org.junit.jupiter.api.Test;
     22import org.junit.jupiter.params.ParameterizedTest;
     23import org.junit.jupiter.params.provider.Arguments;
     24import org.junit.jupiter.params.provider.MethodSource;
     25
     26/**
     27 * Test class for {@link MapboxVectorTileSource}
     28 * @author Taylor Smock
     29 * @since xxx
     30 */
     31class MapboxVectorTileSourceTest {
     32    @RegisterExtension
     33    JOSMTestRules rule = new JOSMTestRules();
     34    private static class SelectLayerDialogMocker extends ExtendedDialogMocker {
     35        int index;
     36        @Override
     37        protected void act(final ExtendedDialog instance) {
     38            ((JosmComboBox<?>) this.getContent(instance)).setSelectedIndex(index);
     39        }
     40
     41        @Override
     42        protected String getString(final ExtendedDialog instance) {
     43            return String.join(";", ((Source) ((JosmComboBox<?>) this.getContent(instance)).getSelectedItem()).getUrls());
     44        }
     45    }
     46
     47    @Test
     48    void testNoStyle() {
     49        MapboxVectorTileSource tileSource = new MapboxVectorTileSource(
     50          new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "pbf/mapillary/{z}/{x}/{y}.mvt"));
     51        assertNull(tileSource.getStyleSource());
     52    }
     53
     54    private static Stream<Arguments> testMapillaryStyle() {
     55        return Stream.of(Arguments.of(0, "Test Mapillary: mapillary-source", "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt"),
     56          Arguments.of(1, "Test Mapillary: mapillary-features-source",
     57            "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_"
     58              + "&layers=points&per_page=1000"),
     59          Arguments.of(2, "Test Mapillary: mapillary-traffic-signs-source",
     60            "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_"
     61              + "&layers=trafficsigns&per_page=1000"));
     62    }
     63
     64    @ParameterizedTest
     65    @MethodSource("testMapillaryStyle")
     66    void testMapillaryStyle(Integer index, String expected, String dialogMockerText) {
     67        TestUtils.assumeWorkingJMockit();
     68        SelectLayerDialogMocker extendedDialogMocker = new SelectLayerDialogMocker();
     69        extendedDialogMocker.index = index;
     70        extendedDialogMocker.getMockResultMap().put(dialogMockerText, "Add layers");
     71        MapboxVectorTileSource tileSource = new MapboxVectorTileSource(
     72          new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "mapillary.json"));
     73        MapBoxVectorStyle styleSource = tileSource.getStyleSource();
     74        assertNotNull(styleSource);
     75        assertEquals(expected, tileSource.toString());
     76    }
     77}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java
    new file mode 100644
    index 000000000..0130da35e
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6
     7import javax.json.Json;
     8import javax.json.JsonValue;
     9
     10import nl.jqno.equalsverifier.EqualsVerifier;
     11import org.junit.jupiter.api.Test;
     12
     13/**
     14 * Test class for {@link Expression}
     15 * @author Taylor Smock
     16 * @since xxx
     17 */
     18class ExpressionTest {
     19    @Test
     20    void testInvalidJson() {
     21        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.NULL));
     22        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.FALSE));
     23        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.TRUE));
     24        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_OBJECT));
     25        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_ARRAY));
     26        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createObjectBuilder().add("bad", "value").build()));
     27        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1)));
     28        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1.0)));
     29        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue("bad string")));
     30    }
     31
     32    @Test
     33    void testBasicExpressions() {
     34        // "filter": [ "==|>=|<=|<|>", "key", "value" ]
     35        assertEquals("[key=value]", new Expression(Json.createArrayBuilder().add("==").add("key").add("value").build()).toString());
     36        assertEquals("[key>=true]", new Expression(Json.createArrayBuilder().add(">=").add("key").add(true).build()).toString());
     37        assertEquals("[key<=false]", new Expression(Json.createArrayBuilder().add("<=").add("key").add(false).build()).toString());
     38        assertEquals("[key<1]", new Expression(Json.createArrayBuilder().add("<").add("key").add(1).build()).toString());
     39        assertEquals("[key>2.5]", new Expression(Json.createArrayBuilder().add(">").add("key").add(2.5).build()).toString());
     40        // Test bad expression
     41        assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createArrayBuilder().add(">>").add("key").add("value").build()));
     42
     43        // Test expressions with a subarray and object. This is expected to fail when properly supported, so it should be fixed.
     44        assertEquals("[key=[{bad:value}]]", new Expression(Json.createArrayBuilder().add("==").add("key").add(
     45          Json.createArrayBuilder().add(Json.createObjectBuilder().add("bad", "value"))).build()).toString());
     46        assertEquals("[key=]", new Expression(Json.createArrayBuilder().add("==").add("key").add(JsonValue.NULL).build()).toString());
     47    }
     48
     49    @Test
     50    void testEquals() {
     51        EqualsVerifier.forClass(Expression.class).verify();
     52    }
     53}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
    new file mode 100644
    index 000000000..28b09b950
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNull;
     6import static org.junit.jupiter.api.Assertions.assertSame;
     7import static org.junit.jupiter.api.Assertions.assertThrows;
     8
     9import java.text.MessageFormat;
     10import java.util.Locale;
     11
     12import javax.json.Json;
     13import javax.json.JsonObject;
     14import javax.json.JsonValue;
     15
     16import nl.jqno.equalsverifier.EqualsVerifier;
     17import org.junit.jupiter.api.Test;
     18
     19/**
     20 * Test class for {@link Layers}.
     21 * @implNote Tests will fail when support is added for new styling information.
     22 * All current (2021-03-31) properties are checked for in some form or another.
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26class LayersTest {
     27    @Test
     28    void testBackground() {
     29        // Test an empty background layer
     30        Layers emptyBackgroundLayer = new Layers(Json.createObjectBuilder()
     31          .add("type", Layers.Type.BACKGROUND.name())
     32          .add("id", "Empty Background").build());
     33        assertEquals("Empty Background", emptyBackgroundLayer.getId());
     34        assertEquals(Layers.Type.BACKGROUND, emptyBackgroundLayer.getType());
     35        assertNull(emptyBackgroundLayer.getSource());
     36        assertSame(Expression.EMPTY_EXPRESSION, emptyBackgroundLayer.getFilter());
     37        assertEquals("", emptyBackgroundLayer.toString());
     38
     39        // Test a background layer with some styling information
     40        JsonObject allProperties = Json.createObjectBuilder()
     41          .add("background-color", "#fff000") // fill-color:#fff000;
     42          .add("background-opacity", 0.5) // No good mapping for JOSM yet
     43          .add("background-pattern", "null") // This should be an image, not implemented
     44          .build();
     45        Layers backgroundLayer = new Layers(Json.createObjectBuilder()
     46          .add("id", "Background layer")
     47          .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT))
     48          .add("paint", allProperties)
     49        .build());
     50        assertEquals("canvas{fill-color:#fff000;}", backgroundLayer.toString());
     51
     52        // Test a background layer with some styling information, but invisible
     53        Layers invisibleBackgroundLayer = new Layers(Json.createObjectBuilder()
     54          .add("id", "Background layer")
     55          .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT))
     56          .add("layout", Json.createObjectBuilder().add("visibility", "none").build())
     57          .add("paint", allProperties).build());
     58        assertEquals("", invisibleBackgroundLayer.toString());
     59    }
     60
     61    @Test
     62    void testFill() {
     63        // Test a layer without a source (should fail)
     64        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     65          .add("type", Layers.Type.FILL.name())
     66          .add("id", "Empty Fill").build()));
     67
     68        // Test an empty fill layer
     69        Layers emptyFillLayer = new Layers(Json.createObjectBuilder()
     70          .add("type", Layers.Type.FILL.name())
     71          .add("id", "Empty Fill")
     72          .add("source", "Random source").build());
     73        assertEquals("Empty Fill", emptyFillLayer.getId());
     74        assertEquals("Random source", emptyFillLayer.getSource());
     75        assertEquals("", emptyFillLayer.toString());
     76
     77        // Test a fully implemented fill layer
     78        JsonObject allLayoutProperties = Json.createObjectBuilder()
     79          .add("fill-sort-key", 5)
     80          .add("visibility", "visible")
     81          .build();
     82        JsonObject allPaintProperties = Json.createObjectBuilder()
     83          .add("fill-antialias", false)
     84          .add("fill-color", "#fff000") // fill-color:#fff000
     85          .add("fill-opacity", 0.5) // fill-opacity:0.5
     86          .add("fill-outline-color", "#ffff00") // fill-color:#ffff00 (defaults to fill-color)
     87          .add("fill-pattern", JsonValue.NULL) // disables fill-outline-color and fill-color
     88          .add("fill-translate", Json.createArrayBuilder().add(5).add(5))
     89          .add("fill-translate-anchor", "viewport") // requires fill-translate
     90          .build();
     91
     92        Layers fullFillLayer = new Layers(Json.createObjectBuilder()
     93          .add("type", Layers.Type.FILL.toString())
     94          .add("id", "random-layer-id")
     95          .add("source", "Random source")
     96          .add("layout", allLayoutProperties)
     97          .add("paint", allPaintProperties)
     98          .build());
     99        assertEquals("random-layer-id", fullFillLayer.getId());
     100        assertEquals(Layers.Type.FILL, fullFillLayer.getType());
     101        assertEquals("area::random-layer-id{fill-color:#fff000;fill-opacity:0.5;color:#ffff00;}", fullFillLayer.toString());
     102
     103        // Test a fully implemented fill layer (invisible)
     104        Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder()
     105          .add("type", Layers.Type.FILL.toString())
     106          .add("id", "random-layer-id")
     107          .add("source", "Random source")
     108          .add("layout", Json.createObjectBuilder(allLayoutProperties)
     109            .add("visibility", "none"))
     110          .add("paint", allPaintProperties)
     111          .build());
     112        assertEquals("random-layer-id", fullFillInvisibleLayer.getId());
     113        assertEquals(Layers.Type.FILL, fullFillInvisibleLayer.getType());
     114        assertEquals("", fullFillInvisibleLayer.toString());
     115    }
     116
     117    @Test
     118    void testLine() {
     119        // Test a layer without a source (should fail)
     120        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     121          .add("type", Layers.Type.LINE.name())
     122          .add("id", "Empty Line").build()));
     123
     124        JsonObject allLayoutProperties = Json.createObjectBuilder()
     125          .add("line-cap", "round") // linecap:round;
     126          .add("line-join", "bevel")
     127          .add("line-miter-limit", 65)
     128          .add("line-round-limit", 1.5)
     129          .add("line-sort-key", 3)
     130          .add("visibility", "visible")
     131          .build();
     132        JsonObject allPaintProperties = Json.createObjectBuilder()
     133          .add("line-blur", 5)
     134          .add("line-color", "#fff000") // color:#fff000;
     135          .add("line-dasharray", Json.createArrayBuilder().add(1).add(5).add(1)) // dashes:1,5,1;
     136          .add("line-gap-width", 6)
     137          .add("line-gradient", "#ffff00") // disabled by line-dasharray/line-pattern, source must be "geojson"
     138          .add("line-offset", 12)
     139          .add("line-opacity", 0.5) // opacity:0.5;
     140          .add("line-pattern", JsonValue.NULL)
     141          .add("line-translate", Json.createArrayBuilder().add(-1).add(-2))
     142          .add("line-translate-anchor", "viewport")
     143          .add("line-width", 22) // width:22;
     144          .build();
     145
     146        // Test fully defined line
     147        Layers fullLineLayer = new Layers(Json.createObjectBuilder()
     148          .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT))
     149          .add("id", "random-layer-id")
     150          .add("source", "Random source")
     151          .add("layout", allLayoutProperties)
     152          .add("paint", allPaintProperties)
     153          .build());
     154        assertEquals("random-layer-id", fullLineLayer.getId());
     155        assertEquals(Layers.Type.LINE, fullLineLayer.getType());
     156        assertEquals("way::random-layer-id{color:#fff000;opacity:0.5;linecap:round;dashes:1,5,1;width:22;}", fullLineLayer.toString());
     157
     158        // Test invisible line
     159        Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder()
     160          .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT))
     161          .add("id", "random-layer-id")
     162          .add("source", "Random source")
     163          .add("layout", Json.createObjectBuilder(allLayoutProperties)
     164            .add("visibility", "none"))
     165          .add("paint", allPaintProperties)
     166          .build());
     167        assertEquals("random-layer-id", fullLineInvisibleLayer.getId());
     168        assertEquals(Layers.Type.LINE, fullLineInvisibleLayer.getType());
     169        assertEquals("", fullLineInvisibleLayer.toString());
     170    }
     171
     172    @Test
     173    void testSymbol() {
     174        // Test a layer without a source (should fail)
     175        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     176          .add("type", Layers.Type.SYMBOL.name())
     177          .add("id", "Empty Symbol").build()));
     178
     179        JsonObject allPaintProperties = Json.createObjectBuilder()
     180          .add("icon-color", "#fff000") // also requires sdf icons
     181          .add("icon-halo-blur", 5)
     182          .add("icon-halo-color", "#ffff00")
     183          .add("icon-halo-width", 6)
     184          .add("icon-opacity", 0.5) // icon-opacity:0.5;
     185          .add("icon-translate", Json.createArrayBuilder().add(11).add(12))
     186          .add("icon-translate-anchor", "viewport") // also requires icon-translate
     187          .add("text-color", "#fffff0") // text-color:#fffff0;
     188          .add("text-halo-blur", 15)
     189          .add("text-halo-color", "#ffffff") // text-halo-color:#ffffff;
     190          .add("text-halo-width", 16) // text-halo-radius:16;
     191          .add("text-opacity", 0.6) // text-opacity:0.6;
     192          .add("text-translate", Json.createArrayBuilder().add(26).add(27))
     193          .add("text-translate-anchor", "viewport")
     194          .build();
     195        JsonObject allLayoutProperties = Json.createObjectBuilder()
     196          .add("icon-allow-overlap", true)
     197          .add("icon-anchor", "left")
     198          .add("icon-ignore-placement", true)
     199          .add("icon-image", "random-image") // icon-image:concat(\"random-image\");
     200          .add("icon-keep-upright", true) // also requires icon-rotation-alignment=map and symbol-placement=line|line-center
     201          .add("icon-offset", Json.createArrayBuilder().add(2).add(3)) // icon-offset-x:2.0;icon-offset-y:3.0;
     202          .add("icon-optional", true) // also requires text-field
     203          .add("icon-padding", 4)
     204          .add("icon-pitch-alignment", "viewport")
     205          .add("icon-rotate", 30) // icon-rotation:30.0;
     206          .add("icon-rotation-alignment", "map")
     207          .add("icon-size", 2)
     208          .add("icon-text-fit", "width") // also requires text-field
     209          .add("icon-text-fit-padding", Json.createArrayBuilder().add(7).add(8).add(9).add(10))
     210          .add("symbol-avoid-edges", true)
     211          .add("symbol-placement", "line")
     212          .add("symbol-sort-key", 13)
     213          .add("symbol-spacing", 14) // requires symbol-placement=line
     214          .add("symbol-z-order", "source")
     215          .add("text-allow-overlap", true) // requires text-field
     216          .add("text-anchor", "left") // requires text-field, disabled by text-variable-anchor
     217          .add("text-field", "something") // text:something;
     218          .add("text-font", Json.createArrayBuilder().add("SansSerif")) // DroidSans isn't always available in an IDE
     219          .add("text-ignore-placement", true)
     220          .add("text-justify", "left")
     221          .add("text-keep-upright", false)
     222          .add("text-letter-spacing", 17)
     223          .add("text-line-height", 1.3)
     224          .add("text-max-angle", 18)
     225          .add("text-max-width", 19)
     226          .add("text-offset", Json.createArrayBuilder().add(20).add(21))
     227          .add("text-optional", true)
     228          .add("text-padding", 22)
     229          .add("text-pitch-alignment", "viewport")
     230          .add("text-radial-offset", 23)
     231          .add("text-rotate", 24)
     232          .add("text-rotation-alignment", "viewport")
     233          .add("text-size", 25) // font-size:25;
     234          .add("text-transform", "uppercase")
     235          .add("text-variable-anchor", "left")
     236          .add("text-writing-mode", "vertical")
     237          .add("visibility", "visible").build();
     238
     239        // Test fully defined symbol
     240        Layers fullLineLayer = new Layers(Json.createObjectBuilder()
     241          .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
     242          .add("id", "random-layer-id")
     243          .add("source", "Random source")
     244          .add("layout", allLayoutProperties)
     245          .add("paint", allPaintProperties)
     246          .build());
     247        assertEquals("random-layer-id", fullLineLayer.getId());
     248        assertEquals(Layers.Type.SYMBOL, fullLineLayer.getType());
     249        assertEquals("node::random-layer-id{icon-image:concat(\"random-image\");icon-offset-x:2.0;icon-offset-y:3.0;"
     250          + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
     251          + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", fullLineLayer.toString());
     252
     253        // Test an invisible symbol
     254        Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder()
     255          .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
     256          .add("id", "random-layer-id")
     257          .add("source", "Random source")
     258          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     259          .add("paint", allPaintProperties)
     260          .build());
     261        assertEquals("random-layer-id", fullLineInvisibleLayer.getId());
     262        assertEquals(Layers.Type.SYMBOL, fullLineInvisibleLayer.getType());
     263        assertEquals("", fullLineInvisibleLayer.toString());
     264
     265        // Test with placeholders in icon-image
     266        Layers fullOneIconImagePlaceholderLineLayer = new Layers(Json.createObjectBuilder()
     267          .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
     268          .add("id", "random-layer-id")
     269          .add("source", "Random source")
     270          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "{value}"))
     271          .add("paint", allPaintProperties)
     272          .build());
     273        assertEquals("node::random-layer-id{icon-image:concat(tag(\"value\"));icon-offset-x:2.0;icon-offset-y:3.0;"
     274          + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
     275          + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
     276          fullOneIconImagePlaceholderLineLayer.toString());
     277
     278        // Test with placeholders in icon-image
     279        Layers fullOneIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder()
     280          .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
     281          .add("id", "random-layer-id")
     282          .add("source", "Random source")
     283          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random"))
     284          .add("paint", allPaintProperties)
     285          .build());
     286        assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random\");icon-offset-x:2.0;"
     287          + "icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";"
     288          + "font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
     289          fullOneIconImagePlaceholderExtraLineLayer.toString());
     290
     291        // Test with placeholders in icon-image
     292        Layers fullTwoIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder()
     293          .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
     294          .add("id", "random-layer-id")
     295          .add("source", "Random source")
     296          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random/{value2}"))
     297          .add("paint", allPaintProperties)
     298          .build());
     299        assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random/\",tag(\"value2\"));"
     300          + "icon-offset-x:2.0;icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;"
     301          + "font-family:\"SansSerif\";font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;"
     302          + "text-opacity:0.6;font-size:25;}", fullTwoIconImagePlaceholderExtraLineLayer.toString());
     303    }
     304
     305    @Test
     306    void testRaster() {
     307        // Test a layer without a source (should fail)
     308        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     309          .add("type", Layers.Type.RASTER.name())
     310          .add("id", "Empty Raster").build()));
     311
     312        JsonObject allPaintProperties = Json.createObjectBuilder()
     313          .add("raster-brightness-max", 0.5)
     314          .add("raster-brightness-min", 0.6)
     315          .add("raster-contrast", 0.7)
     316          .add("raster-fade-duration", 1)
     317          .add("raster-hue-rotate", 2)
     318          .add("raster-opacity", 0.7)
     319          .add("raster-resampling", "nearest")
     320          .add("raster-saturation", 0.8)
     321          .build();
     322        JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
     323
     324        // Test fully defined raster
     325        Layers fullRaster = new Layers(Json.createObjectBuilder()
     326          .add("id", "test-raster")
     327          .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT))
     328          .add("source", "Random source")
     329          .add("layout", allLayoutProperties)
     330          .add("paint", allPaintProperties)
     331          .build());
     332        assertEquals(Layers.Type.RASTER, fullRaster.getType());
     333        assertEquals("test-raster", fullRaster.getId());
     334        assertEquals("Random source", fullRaster.getSource());
     335        assertEquals("", fullRaster.toString());
     336
     337        // Test fully defined invisible raster
     338        Layers fullInvisibleRaster = new Layers(Json.createObjectBuilder()
     339          .add("id", "test-raster")
     340          .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT))
     341          .add("source", "Random source")
     342          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     343          .add("paint", allPaintProperties)
     344          .build());
     345        assertEquals("", fullInvisibleRaster.toString());
     346    }
     347
     348    @Test
     349    void testCircle() {
     350        // Test a layer without a source (should fail)
     351        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     352          .add("type", Layers.Type.CIRCLE.name())
     353          .add("id", "Empty Circle").build()));
     354
     355        JsonObject allPaintProperties = Json.createObjectBuilder()
     356          .add("circle-blur", 1)
     357          .add("circle-color", "#fff000") // symbol-fill-color:#fff000;
     358          .add("circle-opacity", 0.5) // symbol-fill-opacity:0.5;
     359          .add("circle-pitch-alignment", "map")
     360          .add("circle-pitch-scale", "viewport")
     361          .add("circle-radius", 2) // symbol-size:4.0; (we use width)
     362          .add("circle-stroke-color", "#ffff00") // symbol-stroke-color:#ffff00;
     363          .add("circle-stroke-opacity", 0.6) // symbol-stroke-opacity:0.6;
     364          .add("circle-stroke-width", 5) // symbol-stroke-width:5.0;
     365          .add("circle-translate", Json.createArrayBuilder().add(3).add(4))
     366          .add("circle-translate-anchor", "viewport")
     367          .build();
     368        JsonObject allLayoutProperties = Json.createObjectBuilder()
     369          .add("circle-sort-key", 3)
     370          .add("visibility", "visible")
     371          .build();
     372
     373        Layers fullCircleLayer = new Layers(Json.createObjectBuilder()
     374          .add("id", "Full circle layer")
     375          .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT))
     376          .add("source", "Random source")
     377          .add("layout", allLayoutProperties)
     378          .add("paint", allPaintProperties)
     379          .build());
     380        assertEquals(Layers.Type.CIRCLE, fullCircleLayer.getType());
     381        assertEquals("Full circle layer", fullCircleLayer.getId());
     382        assertEquals("Random source", fullCircleLayer.getSource());
     383        assertEquals("node::Full circle layer{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:0.5;"
     384          + "symbol-size:4.0;symbol-stroke-color:#ffff00;symbol-stroke-opacity:0.6;symbol-stroke-width:5;}", fullCircleLayer.toString());
     385
     386        Layers fullCircleInvisibleLayer = new Layers(Json.createObjectBuilder()
     387          .add("id", "Full circle layer")
     388          .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT))
     389          .add("source", "Random source")
     390          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     391          .add("paint", allPaintProperties)
     392          .build());
     393        assertEquals(Layers.Type.CIRCLE, fullCircleInvisibleLayer.getType());
     394        assertEquals("Full circle layer", fullCircleInvisibleLayer.getId());
     395        assertEquals("Random source", fullCircleInvisibleLayer.getSource());
     396        assertEquals("", fullCircleInvisibleLayer.toString());
     397    }
     398
     399    @Test
     400    void testFillExtrusion() {
     401        // Test a layer without a source (should fail)
     402        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     403          .add("type", Layers.Type.FILL_EXTRUSION.name())
     404          .add("id", "Empty Fill Extrusion").build()));
     405
     406        JsonObject allPaintProperties = Json.createObjectBuilder()
     407          .add("fill-extrusion-base", 1)
     408          .add("fill-extrusion-color", "#fff000")
     409          .add("fill-extrusion-height", 2)
     410          .add("fill-extrusion-opacity", 0.5)
     411          .add("fill-extrusion-pattern", "something-random")
     412          .add("fill-extrusion-translate", Json.createArrayBuilder().add(3).add(4))
     413          .add("fill-extrusion-translate-anchor", "viewport")
     414          .add("fill-extrusion-vertical-gradient", false)
     415          .build();
     416        JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
     417
     418        Layers fullFillLayer = new Layers(Json.createObjectBuilder()
     419          .add("id", "Fill Extrusion")
     420          .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT))
     421          .add("source", "Random source")
     422          .add("layout", allLayoutProperties)
     423          .add("paint", allPaintProperties)
     424          .build());
     425        assertEquals("", fullFillLayer.toString());
     426        assertEquals(Layers.Type.FILL_EXTRUSION, fullFillLayer.getType());
     427        Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder()
     428          .add("id", "Fill Extrusion")
     429          .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT))
     430          .add("source", "Random source")
     431          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     432          .add("paint", allPaintProperties)
     433          .build());
     434        assertEquals("", fullFillInvisibleLayer.toString());
     435        assertEquals(Layers.Type.FILL_EXTRUSION, fullFillInvisibleLayer.getType());
     436    }
     437
     438    @Test
     439    void testHeatmap() {
     440        // Test a layer without a source (should fail)
     441        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     442          .add("type", Layers.Type.HEATMAP.name())
     443          .add("id", "Empty Heatmap").build()));
     444
     445        JsonObject allPaintProperties = Json.createObjectBuilder()
     446          .add("heatmap-color", "#fff000") // This will probably be a gradient of some type
     447          .add("heatmap-intensity", 0.5)
     448          .add("heatmap-opacity", 0.6)
     449          .add("heatmap-radius", 1) // This is in pixels
     450          .add("heatmap-weight", 0.7)
     451          .build();
     452        JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
     453
     454        Layers fullHeatmapLayer = new Layers(Json.createObjectBuilder()
     455          .add("id", "Full heatmap")
     456          .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT))
     457          .add("source", "Random source")
     458          .add("paint", allPaintProperties)
     459          .add("layout", allLayoutProperties)
     460          .build());
     461        assertEquals(Layers.Type.HEATMAP, fullHeatmapLayer.getType());
     462        assertEquals("", fullHeatmapLayer.toString());
     463
     464        Layers fullHeatmapInvisibleLayer = new Layers(Json.createObjectBuilder()
     465          .add("id", "Full heatmap")
     466          .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT))
     467          .add("source", "Random source")
     468          .add("paint", allPaintProperties)
     469          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     470          .build());
     471        assertEquals(Layers.Type.HEATMAP, fullHeatmapInvisibleLayer.getType());
     472        assertEquals("", fullHeatmapInvisibleLayer.toString());
     473    }
     474
     475    @Test
     476    void testHillshade() {
     477        // Test a layer without a source (should fail)
     478        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     479          .add("type", Layers.Type.HILLSHADE.name())
     480          .add("id", "Empty Hillshade").build()));
     481
     482        JsonObject allPaintProperties = Json.createObjectBuilder()
     483          .add("hillshade-accent-color", "#fff000")
     484          .add("hillshade-exaggeration", 0.6)
     485          .add("hillshade-highlight-color", "#ffff00")
     486          .add("hillshade-illumination-anchor", "map")
     487          .add("hillshade-illumination-direction", 90)
     488          .add("hillshade-shadow-color", "#fffff0")
     489          .build();
     490        JsonObject allLayoutProperties = Json.createObjectBuilder()
     491          .add("visibility", "visible")
     492          .build();
     493
     494        Layers fullHillshadeLayer = new Layers(Json.createObjectBuilder()
     495          .add("id", "Hillshade")
     496          .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT))
     497          .add("source", "Random source")
     498          .add("paint", allPaintProperties)
     499          .add("layout", allLayoutProperties)
     500          .build());
     501        assertEquals(Layers.Type.HILLSHADE, fullHillshadeLayer.getType());
     502        assertEquals("", fullHillshadeLayer.toString());
     503
     504        Layers fullHillshadeInvisibleLayer = new Layers(Json.createObjectBuilder()
     505          .add("id", "Hillshade")
     506          .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT))
     507          .add("source", "Random source")
     508          .add("paint", allPaintProperties)
     509          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     510          .build());
     511        assertEquals(Layers.Type.HILLSHADE, fullHillshadeInvisibleLayer.getType());
     512        assertEquals("", fullHillshadeInvisibleLayer.toString());
     513    }
     514
     515    @Test
     516    void testSky() {
     517        // Test a layer without a source (should fail)
     518        assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
     519          .add("type", Layers.Type.SKY.name())
     520          .add("id", "Empty Sky").build()));
     521
     522        JsonObject allPaintProperties = Json.createObjectBuilder()
     523          .add("sky-atmosphere-color", "red")
     524          .add("sky-atmosphere-halo-color", "yellow")
     525          // 360180 is apparently included in this? Or it might be a formatting issue in the docs.
     526          .add("sky-atmosphere-sun", Json.createArrayBuilder().add(0, 360180))
     527          .add("sky-atmosphere-sun-intensity", 99)
     528          .add("sky-gradient", "#fff000")
     529          .add("sky-gradient-center", Json.createArrayBuilder().add(0).add(360180)) // see note on 360180 above
     530          .add("sky-gradient-radius", 1)
     531          .add("sky-opacity", 0.5)
     532          .add("sky-type", "gradient")
     533          .build();
     534        JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
     535
     536        Layers fullSkyLayer = new Layers(Json.createObjectBuilder()
     537          .add("id", "Sky")
     538          .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT))
     539          .add("source", "Random source")
     540          .add("paint", allPaintProperties)
     541          .add("layout", allLayoutProperties)
     542          .build());
     543        assertEquals(Layers.Type.SKY, fullSkyLayer.getType());
     544        assertEquals("", fullSkyLayer.toString());
     545
     546        Layers fullSkyInvisibleLayer = new Layers(Json.createObjectBuilder()
     547          .add("id", "Sky")
     548          .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT))
     549          .add("source", "Random source")
     550          .add("paint", allPaintProperties)
     551          .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
     552          .build());
     553        assertEquals(Layers.Type.SKY, fullSkyInvisibleLayer.getType());
     554        assertEquals("", fullSkyInvisibleLayer.toString());
     555    }
     556
     557    @Test
     558    void testZoomLevels() {
     559        JsonObject baseInformation = Json.createObjectBuilder()
     560          .add("id", "dots")
     561          .add("type", "CiRcLe")
     562          .add("source", "osm-source")
     563          .add("source-layer", "osm-images")
     564          .add("paint", Json.createObjectBuilder()
     565            .add("circle-color", "#fff000")
     566            .add("circle-radius", 6)
     567          ).build();
     568        Layers noZoomLayer = new Layers(baseInformation);
     569        String baseString = "node{0}::dots'{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:1;"
     570          + "symbol-size:12.0;symbol-stroke-color:#000000;symbol-stroke-opacity:1;symbol-stroke-width:0;}'";
     571        assertEquals("osm-images", noZoomLayer.getSourceLayer());
     572        assertEquals(MessageFormat.format(baseString, ""), noZoomLayer.toString());
     573
     574        Layers minZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
     575          .add("minzoom", 0)
     576          .build());
     577        assertEquals(MessageFormat.format(baseString, "|z0-"), minZoomLayer.toString());
     578
     579        Layers maxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
     580          .add("maxzoom", 24)
     581          .build());
     582        assertEquals(MessageFormat.format(baseString, "|z-24"), maxZoomLayer.toString());
     583
     584        Layers minMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
     585          .add("minzoom", 1)
     586          .add("maxzoom", 2)
     587          .build());
     588        assertEquals(MessageFormat.format(baseString, "|z1-2"), minMaxZoomLayer.toString());
     589
     590        Layers sameMinMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
     591          .add("minzoom", 2)
     592          .add("maxzoom", 2)
     593          .build());
     594        assertEquals(MessageFormat.format(baseString, "|z2"), sameMinMaxZoomLayer.toString());
     595    }
     596
     597    @Test
     598    void testEquals() {
     599        EqualsVerifier.forClass(Layers.class).usingGetClass().verify();
     600    }
     601}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
    new file mode 100644
    index 000000000..1fcb7bfe8
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertFalse;
     7import static org.junit.jupiter.api.Assertions.assertNotNull;
     8import static org.junit.jupiter.api.Assertions.assertThrows;
     9import static org.junit.jupiter.api.Assertions.assertTrue;
     10import static org.junit.jupiter.api.Assertions.fail;
     11
     12
     13import java.awt.Color;
     14import java.awt.Graphics2D;
     15import java.awt.image.BufferedImage;
     16import java.io.ByteArrayInputStream;
     17import java.io.File;
     18import java.io.FileOutputStream;
     19import java.io.IOException;
     20import java.nio.charset.StandardCharsets;
     21import java.nio.file.Paths;
     22import java.text.MessageFormat;
     23import java.util.ArrayList;
     24import java.util.Collection;
     25import java.util.Map;
     26import java.util.Objects;
     27import java.util.Optional;
     28import java.util.concurrent.atomic.AtomicBoolean;
     29import java.util.stream.Collectors;
     30
     31import javax.imageio.ImageIO;
     32import javax.json.Json;
     33import javax.json.JsonObject;
     34import javax.json.JsonObjectBuilder;
     35import javax.json.JsonReader;
     36import javax.json.JsonStructure;
     37import javax.json.JsonValue;
     38
     39import org.openstreetmap.josm.TestUtils;
     40import org.openstreetmap.josm.gui.MainApplication;
     41import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     42import org.openstreetmap.josm.gui.mappaint.Keyword;
     43import org.openstreetmap.josm.gui.mappaint.StyleSource;
     44import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
     45import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
     46import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
     47import org.openstreetmap.josm.testutils.JOSMTestRules;
     48import org.openstreetmap.josm.tools.ColorHelper;
     49import org.openstreetmap.josm.tools.ImageProvider;
     50
     51import nl.jqno.equalsverifier.EqualsVerifier;
     52import org.awaitility.Awaitility;
     53import org.awaitility.Durations;
     54import org.junit.jupiter.api.Test;
     55import org.junit.jupiter.api.extension.RegisterExtension;
     56import org.junit.jupiter.api.io.TempDir;
     57
     58/**
     59 * Test class for {@link MapBoxVectorStyle}
     60 * @author Taylor Smock
     61 */
     62public class MapBoxVectorStyleTest {
     63    /** Used to store sprite files (specifically, sprite{,@2x}.{png,json}) */
     64    @TempDir
     65    File spritesDirectory;
     66
     67    // Needed for osm primitives (we really just need to initialize the config)
     68    // OSM primitives are called when we load style sources
     69    @RegisterExtension
     70    JOSMTestRules rules = new JOSMTestRules();
     71
     72    /** The base information */
     73    private static final String BASE_STYLE = "'{'\"version\":8,\"name\":\"test style\",\"owner\":\"josm test\",\"id\":\"{0}\",{1}'}'";
     74    /** Source 1 */
     75    private static final String SOURCE1 = "\"source1\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}/{x}/{y}.mvt\"]}";
     76    /** Layer 1 */
     77    private static final String LAYER1 = "{\"id\":\"layer1\",\"type\":\"circle\",\"source\":\"source1\",\"source-layer\":\"nodes\"}";
     78    /** Source 2 */
     79    private static final String SOURCE2 = "\"source2\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}2/{x}/{y}.mvt\"]}";
     80    /** Layer 2 */
     81    private static final String LAYER2 = "{\"id\":\"layer2\",\"type\":\"circle\",\"source\":\"source2\",\"source-layer\":\"nodes\"}";
     82
     83    /**
     84     * Check that the version matches the supported style version(s). Currently, only version 8 exists and is (partially)
     85     * supported.
     86     */
     87    @Test
     88    void testVersionChecks() {
     89        assertThrows(NullPointerException.class, () -> new MapBoxVectorStyle(JsonValue.EMPTY_JSON_OBJECT));
     90        IllegalArgumentException badVersion = assertThrows(IllegalArgumentException.class,
     91          () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 7).build()));
     92        assertEquals("Vector Tile Style Version not understood: version 7 (json: {\"version\":7})", badVersion.getMessage());
     93        badVersion = assertThrows(IllegalArgumentException.class,
     94          () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 9).build()));
     95        assertEquals("Vector Tile Style Version not understood: version 9 (json: {\"version\":9})", badVersion.getMessage());
     96        assertDoesNotThrow(() -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 8).build()));
     97    }
     98
     99    @Test
     100    void testSources() {
     101        // Check with an invalid sources list
     102        assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
     103        Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
     104          MessageFormat.format("\"sources\":'{'{0},{1},\"source3\":[\"bad source\"]'}',\"layers\":[{2},{3},{4}]",
     105            SOURCE1, SOURCE2, LAYER1, LAYER2, LAYER2.replace('2', '3'))))).getSources();
     106        assertEquals(3, sources.size());
     107        assertTrue(sources.containsKey(null)); // This is due to there being no source3 layer
     108        sources.remove(null); // Avoid null checks later
     109        assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source1"::equals));
     110        assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source2"::equals));
     111        assertTrue(sources.keySet().stream().map(Source::getName).noneMatch("source3"::equals));
     112    }
     113
     114    @Test
     115    void testSavedFiles() {
     116        assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
     117        Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
     118          MessageFormat.format("\"sources\":'{'{0},{1}'}',\"layers\":[{2},{3}]", SOURCE1, SOURCE2, LAYER1, LAYER2)))).getSources();
     119        assertEquals(2, sources.size());
     120        // For various reasons, the map _must_ be reliably ordered in the order of encounter
     121        Source source1 = sources.keySet().iterator().next();
     122        Source source2 = sources.keySet().stream().skip(1).findFirst().orElseGet(() -> fail("No second source"));
     123        assertEquals("source1", source1.getName());
     124        assertEquals("source2", source2.getName());
     125
     126        // Check that the files have been saved. Ideally, we would check that they haven't been
     127        // saved earlier, since this is in a different thread. Unfortunately, that is a _race condition_.
     128        MapCSSStyleSource styleSource1 = (MapCSSStyleSource) sources.get(source1).getStyleSources().get(0);
     129        MapCSSStyleSource styleSource2 = (MapCSSStyleSource) sources.get(source2).getStyleSources().get(0);
     130
     131        AtomicBoolean saveFinished = new AtomicBoolean();
     132        MainApplication.worker.execute(() -> saveFinished.set(true));
     133        Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get);
     134
     135        assertTrue(styleSource1.url.endsWith("source1.mapcss"));
     136        assertTrue(styleSource2.url.endsWith("source2.mapcss"));
     137
     138        MapCSSStyleSource mapCSSStyleSource1 = new MapCSSStyleSource(styleSource1.url, styleSource1.name, styleSource1.title);
     139        MapCSSStyleSource mapCSSStyleSource2 = new MapCSSStyleSource(styleSource2.url, styleSource2.name, styleSource2.title);
     140
     141        assertEquals(styleSource1, mapCSSStyleSource1);
     142        assertEquals(styleSource2, mapCSSStyleSource2);
     143    }
     144
     145    @Test
     146    void testSprites() throws IOException {
     147        generateSprites(false);
     148        // Ensure that we fall back to 1x sprites
     149        assertTrue(new File(this.spritesDirectory, "sprite.png").exists());
     150        assertFalse(new File(this.spritesDirectory, "sprite@2x.png").exists());
     151        assertTrue(new File(this.spritesDirectory, "sprite.json").exists());
     152        assertFalse(new File(this.spritesDirectory, "sprite@2x.json").exists());
     153
     154        checkImages(false);
     155
     156        generateSprites(true);
     157        checkImages(true);
     158    }
     159
     160    private void checkImages(boolean hiDpi) {
     161        // Ensure that we don't have images saved in the ImageProvider cache
     162        ImageProvider.clearCache();
     163        int hiDpiScalar = hiDpi ? 2 : 1;
     164        String spritePath = new File(this.spritesDirectory, "sprite").getPath();
     165        MapBoxVectorStyle style = new MapBoxVectorStyle(getJson(JsonObject.class,
     166          MessageFormat.format(BASE_STYLE, "sprite_test", "\"sprite\":\"file:/" + spritePath + "\"")));
     167        assertEquals("file:/" + spritePath, style.getSpriteUrl());
     168
     169        AtomicBoolean saveFinished = new AtomicBoolean();
     170        MainApplication.worker.execute(() -> saveFinished.set(true));
     171        Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get);
     172
     173        int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number)
     174        for (int x = 0; x < 3; x++) {
     175            for (int y = 0; y < 3; y++) {
     176                // Expected color
     177                Color color = new Color(scalar * x, scalar * y, scalar * x * y);
     178                int finalX = x;
     179                int finalY = y;
     180                BufferedImage image = (BufferedImage) assertDoesNotThrow(
     181                  () -> ImageProvider.get(new File("test style", MessageFormat.format("({0},{1})", finalX, finalY)).getPath()))
     182                  .getImage();
     183                assertEquals(3 * hiDpiScalar, image.getWidth(null));
     184                assertEquals(3 * hiDpiScalar, image.getHeight(null));
     185                for (int x2 = 0; x2 < image.getWidth(null); x2++) {
     186                    for (int y2 = 0; y2 < image.getHeight(null); y2++) {
     187                        assertEquals(color.getRGB(), image.getRGB(x2, y2));
     188                    }
     189                }
     190            }
     191        }
     192    }
     193
     194    private void generateSprites(boolean hiDpi) throws IOException {
     195        // Create a 3x3 grid of 3x3 or 6x6 pixel squares (depends upon the dpi setting)
     196        int hiDpiScale = hiDpi ? 2 : 1;
     197        BufferedImage nineByNine = new BufferedImage(hiDpiScale * 9, hiDpiScale * 9, BufferedImage.TYPE_4BYTE_ABGR);
     198        int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number)
     199        Graphics2D g = nineByNine.createGraphics();
     200        JsonObjectBuilder json = Json.createObjectBuilder();
     201        for (int x = 0; x < 3; x++) {
     202            for (int y = 0; y < 3; y++) {
     203                Color color = new Color(scalar * x, scalar * y, scalar * x * y);
     204                g.setColor(color);
     205                g.drawRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale);
     206                g.fillRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale);
     207
     208                JsonObjectBuilder sprite = Json.createObjectBuilder();
     209                sprite.add("height", hiDpiScale * 3);
     210                sprite.add("pixelRatio", hiDpiScale);
     211                sprite.add("width", hiDpiScale * 3);
     212                sprite.add("x", 3 * hiDpiScale * x);
     213                sprite.add("y", 3 * hiDpiScale * y);
     214
     215                json.add(MessageFormat.format("({0},{1})", x, y), sprite);
     216            }
     217        }
     218        String imageName = hiDpi ? "sprite@2x.png" : "sprite.png";
     219        ImageIO.write(nineByNine, "png", new File(this.spritesDirectory, imageName));
     220        String jsonName = hiDpi ? "sprite@2x.json" : "sprite.json";
     221        File jsonFile = new File(this.spritesDirectory, jsonName);
     222        try (FileOutputStream fileOutputStream = new FileOutputStream(jsonFile)) {
     223            fileOutputStream.write(json.build().toString().getBytes(StandardCharsets.UTF_8));
     224        }
     225    }
     226
     227    private static <T extends JsonStructure> T getJson(Class<T> clazz, String json) {
     228        try (JsonReader reader = Json.createReader(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)))) {
     229            JsonStructure structure = reader.read();
     230            if (clazz.isAssignableFrom(structure.getClass())) {
     231                return clazz.cast(structure);
     232            }
     233        }
     234        fail("Could not cast to expected class");
     235        throw new IllegalArgumentException();
     236    }
     237
     238    @Test
     239    void testMapillaryStyle() {
     240        final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString();
     241        final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file);
     242        assertNotNull(style);
     243        // There are three "sources" in the mapillary.json file
     244        assertEquals(3, style.getSources().size());
     245        final ElemStyles mapillarySource = style.getSources().entrySet().stream()
     246          .filter(source -> "mapillary-source".equals(source.getKey().getName())).map(
     247            Map.Entry::getValue).findAny().orElse(null);
     248        assertNotNull(mapillarySource);
     249        mapillarySource.getStyleSources().forEach(StyleSource::loadStyleSource);
     250        assertEquals(1, mapillarySource.getStyleSources().size());
     251        final MapCSSStyleSource mapillaryCssSource = (MapCSSStyleSource) mapillarySource.getStyleSources().get(0);
     252        assertTrue(mapillaryCssSource.getErrors().isEmpty());
     253        final MapCSSRule mapillaryOverview = getRule(mapillaryCssSource, "node", "mapillary-overview");
     254        assertNotNull(mapillaryOverview);
     255        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-shape", new Keyword("circle"));
     256        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-color", ColorHelper.html2color("#05CB63"));
     257        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-opacity", 0.6f);
     258        // Docs indicate that symbol-size is total width, while we are translating from a radius. So 2 * 4 = 8.
     259        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-size", 8.0f);
     260    }
     261
     262    @Test
     263    void testEqualsContract() {
     264        // We need to "load" the style sources to avoid the verifier from thinking they are equal
     265        StyleSource canvas = new MapCSSStyleSource("meta{title:\"canvas\";}canvas{default-points:false;}");
     266        StyleSource node = new MapCSSStyleSource("meta{title:\"node\";}node{text:ref;}");
     267        node.loadStyleSource();
     268        canvas.loadStyleSource();
     269        EqualsVerifier.forClass(MapBoxVectorStyle.class)
     270          .withPrefabValues(ImageProvider.class, new ImageProvider("cancel"), new ImageProvider("ok"))
     271          .withPrefabValues(StyleSource.class, canvas, node)
     272          .usingGetClass().verify();
     273    }
     274
     275    /**
     276     * Check that an instruction is in a collection of instructions, and return it
     277     * @param instructions The instructions to search
     278     * @param key The key to look for
     279     * @param value The expected value for the key
     280     */
     281    private void assertInInstructions(Collection<Instruction> instructions, String key, Object value) {
     282        // In JOSM, all Instruction objects are AssignmentInstruction objects
     283        Collection<Instruction.AssignmentInstruction> instructionKeys = instructions.stream()
     284          .filter(Instruction.AssignmentInstruction.class::isInstance)
     285          .map(Instruction.AssignmentInstruction.class::cast).filter(instruction -> Objects.equals(key, instruction.key))
     286          .collect(Collectors.toList());
     287        Optional<Instruction.AssignmentInstruction> instructionOptional = instructionKeys.stream()
     288          .filter(instruction -> Objects.equals(value, instruction.val)).findAny();
     289        assertTrue(instructionOptional.isPresent(), MessageFormat
     290          .format("Expected {0}, but got {1}", value, instructionOptional.orElse(instructionKeys.stream().findAny()
     291            .orElseThrow(() -> new AssertionError("No instruction with "+key+" found"))).val));
     292    }
     293
     294    private static MapCSSRule getRule(MapCSSStyleSource source, String base, String subpart) {
     295        // We need to do a new arraylist just to avoid the occasional ConcurrentModificationException
     296        return new ArrayList<>(source.rules).stream().filter(rule -> rule.selectors.stream()
     297          .anyMatch(selector -> base.equals(selector.getBase()) && subpart.equals(selector.getSubpart().getId(null))))
     298          .findAny().orElse(null);
     299    }
     300}
  • new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java

    diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
    new file mode 100644
    index 000000000..500b5f8b5
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertNull;
     7import static org.junit.jupiter.api.Assertions.assertThrows;
     8import static org.junit.jupiter.api.Assertions.assertTrue;
     9
     10
     11import java.util.Locale;
     12import java.util.stream.Collectors;
     13import java.util.stream.Stream;
     14
     15import javax.json.Json;
     16import javax.json.JsonObject;
     17import javax.json.JsonValue;
     18
     19import org.openstreetmap.josm.data.Bounds;
     20
     21import nl.jqno.equalsverifier.EqualsVerifier;
     22import org.junit.jupiter.api.Test;
     23
     24/**
     25 * Test class for {@link Source}
     26 * @author Taylor Smock
     27 * @since xxx
     28 */
     29public class SourceTest {
     30    @Test
     31    void testEquals() {
     32        EqualsVerifier.forClass(Source.class).usingGetClass().verify();
     33    }
     34
     35    @Test
     36    void testSimpleSources() {
     37        final JsonObject emptyObject = Json.createObjectBuilder().build();
     38        assertThrows(NullPointerException.class, () -> new Source("Test source", emptyObject));
     39
     40        final JsonObject badTypeValue = Json.createObjectBuilder().add("type", "bad type value").build();
     41        assertThrows(IllegalArgumentException.class, () -> new Source("Test source", badTypeValue));
     42
     43        // Only SourceType.{VECTOR,RASTER} are supported
     44        final SourceType[] supported = new SourceType[] {SourceType.VECTOR, SourceType.RASTER};
     45        for (SourceType type : supported) {
     46            final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build();
     47            Source source = assertDoesNotThrow(() -> new Source(type.name(), goodSourceType));
     48            // Check defaults
     49            assertEquals(0, source.getMinZoom());
     50            assertEquals(22, source.getMaxZoom());
     51            assertEquals(type.name(), source.getName());
     52            assertNull(source.getAttributionText());
     53            assertTrue(source.getUrls().isEmpty());
     54            assertEquals(new Bounds(-85.051129, -180, 85.051129, 180), source.getBounds());
     55        }
     56
     57        // Check that unsupported types throw
     58        for (SourceType type : Stream.of(SourceType.values()).filter(t -> Stream.of(supported).noneMatch(t::equals)).collect(
     59          Collectors.toList())) {
     60            final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build();
     61            assertThrows(UnsupportedOperationException.class, () -> new Source(type.name(), goodSourceType));
     62        }
     63    }
     64
     65    @Test
     66    void testTileJsonSpec() {
     67        // This isn't currently implemented, so it should throw. Mostly here to remind implementor to add tests...
     68        final JsonObject tileJsonSpec = Json.createObjectBuilder()
     69          .add("type", SourceType.VECTOR.name()).add("url", "some-random-url.com")
     70          .build();
     71        assertThrows(UnsupportedOperationException.class, () -> new Source("Test TileJson", tileJsonSpec));
     72    }
     73
     74    @Test
     75    void testBounds() {
     76        // Check a "good" bounds
     77        final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
     78          Json.createArrayBuilder().add(-1).add(-2).add(3).add(4)).build();
     79        Source source = new Source("Test Bounds[-1, -2, 3, 4]", tileJsonSpec);
     80        assertEquals(new Bounds(-2, -1, 4, 3), source.getBounds());
     81
     82        // Check "bad" bounds
     83        final JsonObject tileJsonSpecShort = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
     84          Json.createArrayBuilder().add(-1).add(-2).add(3)).build();
     85        IllegalArgumentException badLengthException = assertThrows(IllegalArgumentException.class,
     86          () -> new Source("Test Bounds[-1, -2, 3]", tileJsonSpecShort));
     87        assertEquals("bounds must have four values, but has 3", badLengthException.getMessage());
     88
     89        final JsonObject tileJsonSpecLong = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
     90          Json.createArrayBuilder().add(-1).add(-2).add(3).add(4).add(5)).build();
     91        badLengthException = assertThrows(IllegalArgumentException.class, () -> new Source("Test Bounds[-1, -2, 3, 4, 5]", tileJsonSpecLong));
     92        assertEquals("bounds must have four values, but has 5", badLengthException.getMessage());
     93    }
     94
     95    @Test
     96    void testTiles() {
     97        // No url
     98        final JsonObject tileJsonSpecEmpty = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
     99          JsonValue.NULL).build();
     100        Source source = new Source("Test Tile[]", tileJsonSpecEmpty);
     101        assertTrue(source.getUrls().isEmpty());
     102
     103        // Create a tile URL
     104        final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
     105          Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build();
     106        source = new Source("Test Tile[https://example.org/{bbox-epsg-3857}]", tileJsonSpec);
     107        assertEquals(1, source.getUrls().size());
     108        // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent
     109        assertEquals("https://example.org/{bbox}", source.getUrls().get(0));
     110
     111        // Check with invalid data
     112        final JsonObject tileJsonSpecBad = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
     113          Json.createArrayBuilder().add(1).add("https://example.org/{bbox-epsg-3857}").add(false).add(Json.createArrayBuilder().add("hello"))
     114            .add(Json.createObjectBuilder().add("bad", "array"))).build();
     115        source = new Source("Test Tile[1, https://example.org/{bbox-epsg-3857}, false, [\"hello\"], {\"bad\": \"array\"}]", tileJsonSpecBad);
     116        assertEquals(1, source.getUrls().size());
     117        // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent
     118        assertEquals("https://example.org/{bbox}", source.getUrls().get(0));
     119    }
     120
     121    @Test
     122    void testZoom() {
     123        // Min zoom
     124        final JsonObject minZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom",
     125          5).build();
     126        Source source = new Source("Test Zoom[minzoom=5]", minZoom5);
     127        assertEquals(5, source.getMinZoom());
     128        assertEquals(22, source.getMaxZoom());
     129
     130        // Negative min zoom
     131        final JsonObject minZoomNeg1 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom",
     132          -1).build();
     133        source = new Source("Test Zoom[minzoom=-1]", minZoomNeg1);
     134        assertEquals(0, source.getMinZoom());
     135        assertEquals(22, source.getMaxZoom());
     136
     137        // Max zoom
     138        final JsonObject maxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
     139          5).build();
     140        source = new Source("Test Zoom[maxzoom=5]", maxZoom5);
     141        assertEquals(0, source.getMinZoom());
     142        assertEquals(5, source.getMaxZoom());
     143
     144        // Big Max zoom
     145        final JsonObject maxZoom31 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
     146          31).build();
     147        source = new Source("Test Zoom[maxzoom=31]", maxZoom31);
     148        assertEquals(0, source.getMinZoom());
     149        assertEquals(30, source.getMaxZoom());
     150
     151        // Negative max zoom
     152        final JsonObject maxZoomNeg5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
     153          -5).build();
     154        source = new Source("Test Zoom[maxzoom=-5]", maxZoomNeg5);
     155        assertEquals(0, source.getMinZoom());
     156        assertEquals(0, source.getMaxZoom());
     157
     158        // Min max zoom
     159        final JsonObject minZoom1MaxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
     160          5).add("minzoom", 1).build();
     161        source = new Source("Test Zoom[minzoom=1,maxzoom=5]", minZoom1MaxZoom5);
     162        assertEquals(1, source.getMinZoom());
     163        assertEquals(5, source.getMaxZoom());
     164    }
     165
     166    @Test
     167    void testToString() {
     168        // Simple (no urls)
     169        final JsonObject noTileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).build();
     170        Source source = new Source("Test String[]", noTileJsonSpec);
     171        assertEquals("Test String[]", source.toString());
     172
     173        // With one url
     174        final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
     175          Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build();
     176        source = new Source("Test String[https://example.org/{bbox-epsg-3857}]", tileJsonSpec);
     177        assertEquals("Test String[https://example.org/{bbox-epsg-3857}] https://example.org/{bbox}", source.toString());
     178
     179        // With two URLs
     180        final JsonObject tileJsonSpecMultiple = Json.createObjectBuilder().add("type", SourceType.VECTOR.name())
     181          .add("tiles", Json.createArrayBuilder()
     182            .add("https://example.org/{bbox-epsg-3857}")
     183            .add("https://example.com/{bbox-epsg-3857}")).build();
     184        source = new Source("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}]", tileJsonSpecMultiple);
     185        assertEquals("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}] https://example.org/{bbox} "
     186          + "https://example.com/{bbox}", source.toString());
     187    }
     188}
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java

    diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
    new file mode 100644
    index 000000000..bdfdf86b7
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6import org.junit.jupiter.api.Test;
     7
     8/**
     9 * Test class for {@link ProtoBufParser}
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13class ProtoBufParserTest {
     14    /**
     15     * Check that we are appropriately converting values to the "smallest" type
     16     */
     17    @Test
     18    void testConvertLong() {
     19        // No casting due to auto conversions
     20        assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE));
     21        assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE));
     22        assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE));
     23        assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE));
     24        assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE));
     25        assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE));
     26        assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE));
     27        assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE));
     28    }
     29
     30    /**
     31     * Check that zig zags are appropriately encoded.
     32     */
     33    @Test
     34    void testEncodeZigZag() {
     35        assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue());
     36        assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue());
     37        assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue());
     38        assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue());
     39        assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
     40        assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
     41        assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue());
     42        assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue());
     43        // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
     44        assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
     45        assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
     46        assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
     47        assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
     48        assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
     49        assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
     50    }
     51}
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java

    diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
    new file mode 100644
    index 000000000..d0e204c6a
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6
     7import java.io.IOException;
     8
     9import org.junit.jupiter.api.Test;
     10
     11/**
     12 * Test class for specific {@link ProtoBufRecord} functionality
     13 */
     14class ProtoBufRecordTest {
     15    @Test
     16    void testFixed32() throws IOException {
     17        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
     18        ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser);
     19        assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
     20        assertEquals(1f, thirtyTwoBit.asFloat());
     21    }
     22
     23    @Test
     24    void testUnknown() throws IOException {
     25        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
     26        ProtoBufRecord unknown = new ProtoBufRecord(parser);
     27        assertEquals(WireType.UNKNOWN, unknown.getType());
     28        assertEquals(0, unknown.getBytes().length);
     29    }
     30}
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

    diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
    new file mode 100644
    index 000000000..043481efe
    - +  
     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.geom.Ellipse2D;
     9import java.io.ByteArrayInputStream;
     10import java.io.File;
     11import java.io.IOException;
     12import java.io.InputStream;
     13import java.nio.file.Paths;
     14import java.text.MessageFormat;
     15import java.util.ArrayList;
     16import java.util.Collection;
     17import java.util.List;
     18import java.util.stream.Collectors;
     19
     20import org.openstreetmap.josm.TestUtils;
     21import org.openstreetmap.josm.data.coor.LatLon;
     22import org.openstreetmap.josm.data.imagery.ImageryInfo;
     23import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     24import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     25import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     26import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     27import org.openstreetmap.josm.data.osm.BBox;
     28import org.openstreetmap.josm.data.osm.Node;
     29import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.vector.VectorDataSet;
     31import org.openstreetmap.josm.data.vector.VectorNode;
     32import org.openstreetmap.josm.data.vector.VectorWay;
     33import org.openstreetmap.josm.io.Compression;
     34import org.openstreetmap.josm.testutils.JOSMTestRules;
     35
     36import org.junit.jupiter.api.Test;
     37import org.junit.jupiter.api.extension.RegisterExtension;
     38
     39/**
     40 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     41 *
     42 * @author Taylor Smock
     43 * @since xxx
     44 */
     45class ProtoBufTest {
     46    /**
     47     * Convert an int array into a byte array
     48     * @param intArray The int array to convert (NOTE: numbers must be below 255)
     49     * @return A byte array that can be used
     50     */
     51    static byte[] toByteArray(int[] intArray) {
     52        byte[] byteArray = new byte[intArray.length];
     53        for (int i = 0; i < intArray.length; i++) {
     54            if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) {
     55                throw new IllegalArgumentException();
     56            }
     57            byteArray[i] = Integer.valueOf(intArray[i]).byteValue();
     58        }
     59        return byteArray;
     60    }
     61
     62    @RegisterExtension
     63    JOSMTestRules josmTestRules = new JOSMTestRules().preferences();
     64
     65    private Number bytesToVarInt(int... bytes) {
     66        byte[] byteArray = new byte[bytes.length];
     67        for (int i = 0; i < bytes.length; i++) {
     68            byteArray[i] = (byte) bytes[i];
     69        }
     70        return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
     71    }
     72
     73    /**
     74     * Test reading tile from Mapillary ( 14/3248/6258 )
     75     *
     76     * @throws IOException if there is a problem reading the file
     77     */
     78    @Test
     79    void testRead_14_3248_6258() throws IOException {
     80        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3248", "6258.mvt").toFile();
     81        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     82        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     83        assertEquals(2, records.size());
     84        List<Layer> layers = new ArrayList<>();
     85        for (ProtoBufRecord record : records) {
     86            if (record.getField() == Layer.LAYER_FIELD) {
     87                layers.add(new Layer(record.getBytes()));
     88            } else {
     89                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     90            }
     91        }
     92        Layer mapillarySequences = layers.get(0);
     93        Layer mapillaryPictures = layers.get(1);
     94        assertEquals("mapillary-sequences", mapillarySequences.getName());
     95        assertEquals("mapillary-images", mapillaryPictures.getName());
     96        assertEquals(2048, mapillarySequences.getExtent());
     97        assertEquals(2048, mapillaryPictures.getExtent());
     98
     99        assertEquals(1,
     100                mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500).count());
     101        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500)
     102                .findAny().orElse(null);
     103        assertEquals("dpudn262yz6aitu33zh7bl", testSequence.getTags().get("key"));
     104        assertEquals("clnaw3kpokIAe_CsN5Qmiw", testSequence.getTags().get("ikey"));
     105        assertEquals("B1iNjH4Ohn25cRAGPhetfw", testSequence.getTags().get("userkey"));
     106        assertEquals(Long.valueOf(1557535457401L), Long.valueOf(testSequence.getTags().get("captured_at")));
     107        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     108    }
     109
     110    @Test
     111    void testRead_17_26028_50060() throws IOException {
     112        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf")
     113                .toFile();
     114        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     115        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     116        List<Layer> layers = new ArrayList<>();
     117        for (ProtoBufRecord record : records) {
     118            if (record.getField() == Layer.LAYER_FIELD) {
     119                layers.add(new Layer(record.getBytes()));
     120            } else {
     121                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     122            }
     123        }
     124        assertEquals(19, layers.size());
     125        List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty())
     126                .collect(Collectors.toList());
     127        // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
     128        assertEquals(5, dataLayers.size());
     129
     130        // power_generator_area was rendered incorrectly
     131        final Layer powerGeneratorArea = dataLayers.stream()
     132                .filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null);
     133        assertNotNull(powerGeneratorArea);
     134        final int extent = powerGeneratorArea.getExtent();
     135        // 17/26028/50060 bounds
     136        VectorDataSet vectorDataSet = new VectorDataSet();
     137        MVTTile vectorTile1 = new MVTTile(new MapboxVectorTileSource(new ImageryInfo("Test info", "example.org")),
     138                26028, 50060, 17);
     139        vectorTile1.loadImage(Compression.getUncompressedFileInputStream(vectorTile));
     140        vectorDataSet.addTileData(vectorTile1);
     141        vectorDataSet.setZoom(17);
     142        final Way one = new Way();
     143        one.addNode(new Node(new LatLon(39.0687509, -108.5100816)));
     144        one.addNode(new Node(new LatLon(39.0687509, -108.5095751)));
     145        one.addNode(new Node(new LatLon(39.0687169, -108.5095751)));
     146        one.addNode(new Node(new LatLon(39.0687169, -108.5100816)));
     147        one.addNode(one.getNode(0));
     148        one.setOsmId(666293899, 2);
     149        final BBox searchBBox = one.getBBox();
     150        searchBBox.addPrimitive(one, 0.00001);
     151        final Collection<VectorNode> searchedNodes = vectorDataSet.searchNodes(searchBBox);
     152        final Collection<VectorWay> searchedWays = vectorDataSet.searchWays(searchBBox);
     153        assertEquals(4, searchedNodes.size());
     154    }
     155
     156    @Test
     157    void testReadVarInt() {
     158        assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
     159        assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
     160        assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
     161        // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
     162        Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
     163        assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual,
     164                MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE),
     165                        Long.toBinaryString(actual.longValue())));
     166    }
     167
     168    /**
     169     * Test simple message.
     170     * Check that a simple message is readable
     171     *
     172     * @throws IOException - if an IO error occurs
     173     */
     174    @Test
     175    void testSimpleMessage() throws IOException {
     176        ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
     177        ProtoBufRecord record = new ProtoBufRecord(parser);
     178        assertEquals(WireType.VARINT, record.getType());
     179        assertEquals(150, record.asUnsignedVarInt().intValue());
     180    }
     181
     182    @Test
     183    void testSingletonMultiPoint() throws IOException {
     184        Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray(
     185                new int[] {0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e,
     186                        0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65,
     187                        0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27})))
     188                                .allRecords();
     189        List<Layer> layers = new ArrayList<>();
     190        for (ProtoBufRecord record : records) {
     191            if (record.getField() == Layer.LAYER_FIELD) {
     192                layers.add(new Layer(record.getBytes()));
     193            } else {
     194                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     195            }
     196        }
     197        assertEquals(1, layers.size());
     198        assertEquals(1, layers.get(0).getGeometry().size());
     199        Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next();
     200        assertEquals(4016, shape.getCenterX());
     201        assertEquals(2498, shape.getCenterY());
     202    }
     203
     204    @Test
     205    void testZigZag() {
     206        assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
     207        assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
     208        assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
     209        assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
     210    }
     211}
  • new file test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java

    diff --git test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
    new file mode 100644
    index 000000000..38ab53ad2
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertTrue;
     6
     7
     8import java.nio.file.Paths;
     9import java.text.MessageFormat;
     10import java.util.ArrayList;
     11import java.util.Collection;
     12import java.util.Collections;
     13import java.util.HashSet;
     14import java.util.List;
     15import java.util.Map;
     16import java.util.stream.Collectors;
     17
     18import org.openstreetmap.josm.TestUtils;
     19import org.openstreetmap.josm.data.imagery.ImageryInfo;
     20import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     21import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
     22import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     23import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
     24import org.openstreetmap.josm.testutils.JOSMTestRules;
     25
     26import org.awaitility.Awaitility;
     27import org.awaitility.Durations;
     28import org.junit.jupiter.api.BeforeEach;
     29import org.junit.jupiter.api.Test;
     30import org.junit.jupiter.api.extension.RegisterExtension;
     31
     32/**
     33 * A test for {@link VectorDataSet}
     34 */
     35class VectorDataSetTest {
     36    /**
     37     * Make some methods available for this test class
     38     */
     39    private static class MVTLayerMock extends MVTLayer {
     40        private final Collection<MVTTile> finishedLoading = new HashSet<>();
     41
     42        MVTLayerMock(ImageryInfo info) {
     43            super(info);
     44        }
     45
     46        @Override
     47        protected MapboxVectorTileSource getTileSource() {
     48            return super.getTileSource();
     49        }
     50
     51        protected MapBoxVectorCachedTileLoader getTileLoader() {
     52            if (this.tileLoader == null) {
     53                this.tileLoader = this.getTileLoaderFactory().makeTileLoader(this, Collections.emptyMap(), 7200);
     54            }
     55            if (this.tileLoader instanceof MapBoxVectorCachedTileLoader) {
     56                return (MapBoxVectorCachedTileLoader) this.tileLoader;
     57            }
     58            return null;
     59        }
     60
     61        @Override
     62        public void finishedLoading(MVTTile tile) {
     63            super.finishedLoading(tile);
     64            this.finishedLoading.add(tile);
     65        }
     66
     67        public Collection<MVTTile> finishedLoading() {
     68            return this.finishedLoading;
     69        }
     70    }
     71
     72    @RegisterExtension
     73    JOSMTestRules rule = new JOSMTestRules().projection();
     74
     75    /**
     76     * Load arbitrary tiles
     77     * @param layer The layer to add the tiles to
     78     * @param tiles The tiles to load ([z, x, y, z, x, y, ...]) -- must be divisible by three
     79     */
     80    private static void loadTile(MVTLayerMock layer, int... tiles) {
     81        if (tiles.length % 3 != 0 || tiles.length == 0) {
     82            throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component");
     83        }
     84        final MapboxVectorTileSource tileSource = layer.getTileSource();
     85        MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader();
     86        Collection<MVTTile> tilesCollection = new ArrayList<>();
     87        for (int i = 0; i < tiles.length / 3; i++) {
     88            final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]);
     89            tileLoader.createTileLoaderJob(tile).submit();
     90            tilesCollection.add(tile);
     91        }
     92        Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> layer.finishedLoading().size() == tilesCollection
     93          .size());
     94    }
     95
     96    private MVTLayerMock layer;
     97
     98    @BeforeEach
     99    void setup() {
     100        // Create the preconditions for the test
     101        final ImageryInfo info = new ImageryInfo();
     102        info.setName("en", "Test info");
     103        info.setUrl("file:/" + Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "{z}", "{x}", "{y}.mvt"));
     104        layer = new MVTLayerMock(info);
     105    }
     106
     107    @Test
     108    void testNodeDeduplication() {
     109        final VectorDataSet dataSet = this.layer.getData();
     110        assertTrue(dataSet.allPrimitives().isEmpty());
     111
     112        // Set the zoom to 14, as that is the tile we are checking
     113        dataSet.setZoom(14);
     114        loadTile(this.layer, 14, 3248, 6258);
     115
     116        // There _does_ appear to be some kind of race condition though
     117        Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> dataSet.getNodes().size() > 50);
     118        // Actual test
     119        // With Mapillary, only ends of ways should be untagged
     120        // There are 55 actual "nodes" in the data with two nodes for the ends of the way.
     121        // One of the end nodes is a duplicate of an actual node.
     122        assertEquals(56, dataSet.getNodes().size());
     123        assertEquals(1, dataSet.getWays().size());
     124        assertEquals(0, dataSet.getRelations().size());
     125    }
     126
     127    @Test
     128    void testWayDeduplicationSimple() {
     129        final VectorDataSet dataSet = this.layer.getData();
     130        assertTrue(dataSet.allPrimitives().isEmpty());
     131
     132        // Set the zoom to 14, as that is the tile we are checking
     133        dataSet.setZoom(14);
     134        // Load tiles that are next to each other
     135        loadTile(this.layer, 14, 3248, 6258, 14, 3249, 6258);
     136
     137        Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream()
     138          .collect(Collectors.groupingBy(VectorWay::getId));
     139        wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id)));
     140    }
     141}
  • new file test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java

    diff --git test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java
    new file mode 100644
    index 000000000..834b46c4d
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import org.junit.jupiter.api.Test;
     5import org.junit.jupiter.api.extension.RegisterExtension;
     6import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     7import org.openstreetmap.josm.data.coor.EastNorth;
     8import org.openstreetmap.josm.data.coor.LatLon;
     9import org.openstreetmap.josm.data.osm.BBox;
     10import org.openstreetmap.josm.data.osm.INode;
     11import org.openstreetmap.josm.data.osm.IRelation;
     12import org.openstreetmap.josm.data.osm.IWay;
     13import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     14import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     15import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     16import org.openstreetmap.josm.testutils.JOSMTestRules;
     17
     18import java.util.ArrayList;
     19import java.util.Collections;
     20import java.util.List;
     21
     22import static org.junit.jupiter.api.Assertions.assertEquals;
     23import static org.junit.jupiter.api.Assertions.assertFalse;
     24import static org.junit.jupiter.api.Assertions.assertNotNull;
     25import static org.junit.jupiter.api.Assertions.assertSame;
     26import static org.junit.jupiter.api.Assertions.assertTrue;
     27import static org.junit.jupiter.api.Assertions.fail;
     28
     29/**
     30 * Test class for {@link VectorNode}
     31 * @author Taylor Smock
     32 * @since xxx
     33 */
     34class VectorNodeTest {
     35    @RegisterExtension
     36    JOSMTestRules rule = new JOSMTestRules().projection();
     37
     38    @Test
     39    void testLatLon() {
     40        VectorNode node = new VectorNode("test");
     41        assertTrue(Double.isNaN(node.lat()));
     42        assertTrue(Double.isNaN(node.lon()));
     43        LatLon testLatLon = new LatLon(50, -40);
     44        node.setCoor(testLatLon);
     45        assertEquals(50, node.lat());
     46        assertEquals(-40, node.lon());
     47        assertEquals(testLatLon, node.getCoor());
     48    }
     49
     50    @Test
     51    void testSetEastNorth() {
     52        VectorNode node = new VectorNode("test");
     53        LatLon latLon = new LatLon(-1, 5);
     54        EastNorth eastNorth = ProjectionRegistry.getProjection().latlon2eastNorth(latLon);
     55        node.setEastNorth(eastNorth);
     56        assertEquals(-1, node.lat(), 0.0000000001);
     57        assertEquals(5, node.lon(), 0.0000000001);
     58    }
     59
     60    @Test
     61    void testICoordinate() {
     62        VectorNode node = new VectorNode("test");
     63        assertTrue(Double.isNaN(node.lat()));
     64        assertTrue(Double.isNaN(node.lon()));
     65        ICoordinate coord = new ICoordinate() {
     66            @Override
     67            public double getLat() {
     68                return 5;
     69            }
     70
     71            @Override
     72            public void setLat(double lat) {
     73                // No op
     74            }
     75
     76            @Override
     77            public double getLon() {
     78                return -1;
     79            }
     80
     81            @Override
     82            public void setLon(double lon) {
     83                // no op
     84            }
     85        };
     86        node.setCoor(coord);
     87        assertEquals(5, node.lat());
     88        assertEquals(-1, node.lon());
     89    }
     90
     91    @Test
     92    void testUniqueIdGenerator() {
     93        VectorNode node1 = new VectorNode("test");
     94        VectorNode node2 = new VectorNode("test2");
     95        assertSame(node1.getIdGenerator(), node2.getIdGenerator());
     96        assertNotNull(node1.getIdGenerator());
     97    }
     98
     99    @Test
     100    void testNode() {
     101        assertEquals(OsmPrimitiveType.NODE, new VectorNode("test").getType());
     102    }
     103
     104    @Test
     105    void testBBox() {
     106        VectorNode node = new VectorNode("test");
     107        node.setCoor(new LatLon(5, -1));
     108        assertTrue(node.getBBox().bboxIsFunctionallyEqual(new BBox(-1, 5), 0d));
     109    }
     110
     111    @Test
     112    void testVisitor() {
     113        List<VectorNode> visited = new ArrayList<>();
     114        VectorNode node = new VectorNode("test");
     115        node.accept(new PrimitiveVisitor() {
     116            @Override
     117            public void visit(INode n) {
     118                visited.add((VectorNode) n);
     119            }
     120
     121            @Override
     122            public void visit(IWay<?> w) {
     123                fail("Way should not have been visited");
     124            }
     125
     126            @Override
     127            public void visit(IRelation<?> r) {
     128                fail("Relation should not have been visited");
     129            }
     130        });
     131
     132        assertEquals(1, visited.size());
     133        assertSame(node, visited.get(0));
     134    }
     135
     136    @Test
     137    void testIsReferredToByWays() {
     138        VectorWay way = new VectorWay("test");
     139        VectorNode node = new VectorNode("test");
     140        assertFalse(node.isReferredByWays(1));
     141        assertTrue(node.getReferrers(true).isEmpty());
     142        way.setNodes(Collections.singletonList(node));
     143        assertEquals(1, node.getReferrers(true).size());
     144        assertSame(way, node.getReferrers(true).get(0));
     145        // No dataset yet
     146        assertFalse(node.isReferredByWays(1));
     147        VectorDataSet dataSet = new VectorDataSet();
     148        dataSet.addPrimitive(way);
     149        dataSet.addPrimitive(node);
     150        assertTrue(node.isReferredByWays(1));
     151        assertFalse(node.isReferredByWays(2));
     152    }
     153}
  • new file test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java

    diff --git test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java
    new file mode 100644
    index 000000000..941143b25
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import org.junit.jupiter.api.Test;
     5import org.junit.jupiter.api.extension.RegisterExtension;
     6import org.openstreetmap.josm.testutils.JOSMTestRules;
     7
     8import java.util.Arrays;
     9
     10import static org.junit.jupiter.api.Assertions.assertEquals;
     11import static org.junit.jupiter.api.Assertions.assertFalse;
     12import static org.junit.jupiter.api.Assertions.assertSame;
     13import static org.junit.jupiter.api.Assertions.assertThrows;
     14import static org.junit.jupiter.api.Assertions.assertTrue;
     15
     16/**
     17 * Test class for {@link VectorRelation}
     18 * @author Taylor Smock
     19 * @since xxx
     20 */
     21class VectorRelationTest {
     22    @RegisterExtension
     23    JOSMTestRules rule = new JOSMTestRules();
     24
     25    @Test
     26    void testMembers() {
     27        VectorNode node1 = new VectorNode("test");
     28        VectorNode node2 = new VectorNode("test");
     29        VectorWay way1 = new VectorWay("test");
     30        way1.setNodes(Arrays.asList(node1, node2));
     31        VectorRelationMember member1 = new VectorRelationMember("randomRole", node1);
     32        VectorRelationMember member2 = new VectorRelationMember("role2", way1);
     33        assertSame(node1, member1.getMember());
     34        assertSame(node1.getType(), member1.getType());
     35        assertEquals("randomRole", member1.getRole());
     36        assertSame(node1.getId(), member1.getUniqueId());
     37        // Not a way.
     38        assertThrows(ClassCastException.class, member1::getWay);
     39
     40        assertTrue(member1.isNode());
     41        assertFalse(member1.isWay());
     42        assertFalse(member2.isNode());
     43        assertTrue(member2.isWay());
     44    }
     45}
  • new file test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java

    diff --git test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java
    new file mode 100644
    index 000000000..db2367e6b
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.vector;
     3
     4import org.junit.jupiter.api.Test;
     5import org.openstreetmap.josm.data.coor.LatLon;
     6import org.openstreetmap.josm.data.osm.BBox;
     7import org.openstreetmap.josm.data.osm.INode;
     8import org.openstreetmap.josm.data.osm.IRelation;
     9import org.openstreetmap.josm.data.osm.IWay;
     10import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     11import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
     12
     13import java.util.ArrayList;
     14import java.util.Arrays;
     15import java.util.Collections;
     16import java.util.List;
     17
     18import static org.junit.jupiter.api.Assertions.assertEquals;
     19import static org.junit.jupiter.api.Assertions.assertFalse;
     20import static org.junit.jupiter.api.Assertions.assertNull;
     21import static org.junit.jupiter.api.Assertions.assertSame;
     22import static org.junit.jupiter.api.Assertions.assertTrue;
     23import static org.junit.jupiter.api.Assertions.fail;
     24
     25/**
     26 * Test class for {@link VectorWay}
     27 * @author Taylor Smock
     28 * @since xxx
     29 */
     30class VectorWayTest {
     31    @Test
     32    void testBBox() {
     33        VectorNode node1 = new VectorNode("test");
     34        VectorWay way = new VectorWay("test");
     35        way.setNodes(Collections.singletonList(node1));
     36        node1.setCoor(new LatLon(-5, 1));
     37        assertTrue(node1.getBBox().bboxIsFunctionallyEqual(way.getBBox(), 0.0));
     38
     39        VectorNode node2 = new VectorNode("test");
     40        node2.setCoor(new LatLon(-10, 2));
     41
     42        way.setNodes(Arrays.asList(node1, node2));
     43        assertTrue(way.getBBox().bboxIsFunctionallyEqual(new BBox(2, -10, 1, -5), 0.0));
     44    }
     45
     46    @Test
     47    void testIdGenerator() {
     48        assertSame(new VectorWay("test").getIdGenerator(), new VectorWay("test").getIdGenerator());
     49    }
     50
     51    @Test
     52    void testNodes() {
     53        VectorNode node1 = new VectorNode("test");
     54        VectorNode node2 = new VectorNode("test");
     55        VectorNode node3 = new VectorNode("test");
     56        node1.setId(1);
     57        node2.setId(2);
     58        node3.setId(3);
     59        VectorWay way = new VectorWay("test");
     60        assertNull(way.firstNode());
     61        assertNull(way.lastNode());
     62        assertFalse(way.isClosed());
     63        assertFalse(way.isFirstLastNode(node1));
     64        assertFalse(way.isInnerNode(node2));
     65        way.setNodes(Arrays.asList(node1, node2, node3));
     66        assertEquals(3, way.getNodesCount());
     67        assertEquals(node1, way.getNode(0));
     68        assertEquals(node2, way.getNode(1));
     69        assertEquals(node3, way.getNode(2));
     70        assertTrue(way.isFirstLastNode(node1));
     71        assertTrue(way.isFirstLastNode(node3));
     72        assertFalse(way.isFirstLastNode(node2));
     73        assertTrue(way.isInnerNode(node2));
     74        assertFalse(way.isInnerNode(node1));
     75        assertFalse(way.isInnerNode(node3));
     76
     77        assertEquals(1, way.getNodeIds().get(0));
     78        assertEquals(2, way.getNodeIds().get(1));
     79        assertEquals(3, way.getNodeIds().get(2));
     80        assertEquals(1, way.getNodeId(0));
     81        assertEquals(2, way.getNodeId(1));
     82        assertEquals(3, way.getNodeId(2));
     83
     84        assertFalse(way.isClosed());
     85        assertEquals(OsmPrimitiveType.WAY, way.getType());
     86        List<VectorNode> nodes = new ArrayList<>(way.getNodes());
     87        nodes.add(nodes.get(0));
     88        way.setNodes(nodes);
     89        assertTrue(way.isClosed());
     90        assertEquals(OsmPrimitiveType.CLOSEDWAY, way.getType());
     91    }
     92
     93    @Test
     94    void testAccept() {
     95        VectorWay way = new VectorWay("test");
     96        List<VectorWay> visited = new ArrayList<>(1);
     97        way.accept(new PrimitiveVisitor() {
     98            @Override
     99            public void visit(INode n) {
     100                fail("No nodes should be visited");
     101            }
     102
     103            @Override
     104            public void visit(IWay<?> w) {
     105                visited.add((VectorWay) w);
     106            }
     107
     108            @Override
     109            public void visit(IRelation<?> r) {
     110                fail("No relations should be visited");
     111            }
     112        });
     113
     114        assertEquals(1, visited.size());
     115        assertSame(way, visited.get(0));
     116    }
     117}