Ticket #17177: 17177.6.patch

File 17177.6.patch, 273.5 KB (added by taylor.smock, 4 years ago)

Styling is used now, icons need some additional special handling (not yet implemented)

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/resources/images/dialogs/add_mvt.svg b/resources/images/dialogs/add_mvt.svg
    new file mode 100644
    - +  
     1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
     2<svg
     3        xmlns:dc="http://purl.org/dc/elements/1.1/"
     4        xmlns:cc="http://creativecommons.org/ns#"
     5        xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     6        xmlns="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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.cache;
    33
     4import java.io.File;
     5import java.io.FileInputStream;
    46import java.io.FileNotFoundException;
    57import java.io.IOException;
    68import java.net.HttpURLConnection;
     
    1719import java.util.concurrent.TimeUnit;
    1820import java.util.regex.Matcher;
    1921
    20 import org.apache.commons.jcs3.access.behavior.ICacheAccess;
    21 import org.apache.commons.jcs3.engine.behavior.ICacheElement;
    2222import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
    2323import org.openstreetmap.josm.data.imagery.TileJobOptions;
    2424import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    2727import org.openstreetmap.josm.tools.Logging;
    2828import org.openstreetmap.josm.tools.Utils;
    2929
     30import org.apache.commons.compress.utils.IOUtils;
     31import org.apache.commons.jcs3.access.behavior.ICacheAccess;
     32import org.apache.commons.jcs3.engine.behavior.ICacheElement;
     33
    3034/**
    3135 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
    3236 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
     
    294298        if (attributes == null) {
    295299            attributes = new CacheEntryAttributes();
    296300        }
     301        final URL url = this.getUrlNoException();
     302        if (url == null) {
     303            return false;
     304        }
     305
     306        if (url.getProtocol().contains("http")) {
     307            return loadObjectHttp();
     308        }
     309        if (url.getProtocol().contains("file")) {
     310            return loadObjectFile(url);
     311        }
     312
     313        return false;
     314    }
     315
     316    private boolean loadObjectFile(URL url) {
     317        String fileName = url.toExternalForm();
     318        File file = new File(fileName.substring("file:/".length() - 1));
     319        if (!file.exists()) {
     320            file = new File(fileName.substring("file://".length() - 1));
     321        }
     322        try (FileInputStream fileInputStream = new FileInputStream(file)) {
     323            cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream));
     324            cache.put(getCacheKey(), cacheData, attributes);
     325            return true;
     326        } catch (IOException e) {
     327            Logging.error(e);
     328            attributes.setError(e);
     329            attributes.setException(e);
     330        }
     331        return false;
     332    }
     333
     334    /**
     335     * @return true if object was successfully downloaded via http, false, if there was a loading failure
     336     */
     337    private boolean loadObjectHttp() {
    297338        try {
    298339            // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
    299340            // then just use HEAD request and check returned values
     
    553594        try {
    554595            return getUrl();
    555596        } catch (IOException e) {
     597            Logging.trace(e);
    556598            return null;
    557599        }
    558600    }
  • src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
    a b  
    1010import java.nio.charset.StandardCharsets;
    1111import java.util.HashSet;
    1212import java.util.List;
     13import java.util.Locale;
    1314import java.util.Map;
    1415import java.util.Map.Entry;
    1516import java.util.Optional;
     
    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;
     
    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 *
     
    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;
     
    321325            if (content.length > 0) {
    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/mapbox/Command.java

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

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

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

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

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

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.io.IOException;
     6import java.util.ArrayList;
     7import java.util.Arrays;
     8import java.util.Collection;
     9import java.util.Collections;
     10import java.util.HashSet;
     11import java.util.List;
     12import java.util.Map;
     13import java.util.Objects;
     14import java.util.function.Function;
     15import java.util.stream.Collectors;
     16
     17import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     18import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     19import org.openstreetmap.josm.tools.Logging;
     20
     21/**
     22 * A Mapbox Vector Tile Layer
     23 * @author Taylor Smock
     24 * @since xxx
     25 */
     26public class Layer {
     27    private static final class ValueFields<T> {
     28        static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
     29        static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
     30        static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
     31        static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
     32        // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
     33        static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
     34        static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
     35        static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
     36
     37        /**
     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    /** The shapes to use to draw this layer */
     94    private final List<Geometry> geometryCollection;
     95
     96    /**
     97     * Create a layer from a collection of records
     98     * @param records The records to convert to a layer
     99     * @throws IOException - if an IO error occurs
     100     */
     101    public Layer(Collection<ProtoBufRecord> records) throws IOException {
     102        // Do the unique required fields first
     103        Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
     104        this.version = sorted.get((int) VERSION_FIELD).parallelStream().map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue)
     105                .findFirst().orElse(DEFAULT_VERSION);
     106        // Per spec, we cannot continue past this until we have checked the version number
     107        if (this.version != 1 && this.version != 2) {
     108            throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
     109        }
     110        this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
     111                .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
     112        this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt)
     113                .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
     114
     115        sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
     116                .forEachOrdered(this.keyList::add);
     117        sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes)
     118                .map(ProtoBufParser::new).map(parser1 -> {
     119                    try {
     120                        return new ProtoBufRecord(parser1);
     121                    } catch (IOException e) {
     122                        Logging.error(e);
     123                        return null;
     124                    }
     125                })
     126                .filter(Objects::nonNull)
     127                .map(value -> ValueFields.MAPPERS.parallelStream()
     128                        .filter(v -> v.getField() == value.getField())
     129                        .map(v -> v.convertValue(value)).findFirst()
     130                        .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField()))))
     131                .forEachOrdered(this.valueList::add);
     132        Collection<IOException> exceptions = new HashSet<>(0);
     133        this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> {
     134            try {
     135                return new Feature(this, feature);
     136            } catch (IOException e) {
     137                exceptions.add(e);
     138            }
     139            return null;
     140        }).collect(Collectors.toList());
     141        this.geometryCollection = this.featureCollection.stream().map(Geometry::new).collect(Collectors.toList());
     142        if (!exceptions.isEmpty()) {
     143            throw exceptions.iterator().next();
     144        }
     145        // Cleanup bytes (for memory)
     146        for (ProtoBufRecord record : records) {
     147            record.close();
     148        }
     149    }
     150
     151    /**
     152     * Get all the records from a array of bytes
     153     * @param bytes The byte information
     154     * @return All the protobuf records
     155     * @throws IOException If there was an error reading the bytes (unlikely)
     156     */
     157    private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException {
     158        try (ProtoBufParser parser = new ProtoBufParser(bytes)) {
     159            return parser.allRecords();
     160        }
     161    }
     162
     163    /**
     164     * Create a new layer
     165     * @param bytes The bytes that the layer comes from
     166     * @throws IOException - if an IO error occurs
     167     */
     168    public Layer(byte[] bytes) throws IOException {
     169        this(getAllRecords(bytes));
     170    }
     171
     172    /**
     173     * Get the extent of the tile
     174     * @return The layer extent
     175     */
     176    public int getExtent() {
     177        return this.extent;
     178    }
     179
     180    /**
     181     * Get the feature on this layer
     182     * @return the features
     183     */
     184    public Collection<Feature> getFeatures() {
     185        return Collections.unmodifiableCollection(this.featureCollection);
     186    }
     187
     188    /**
     189     * Get the geometry for this layer
     190     * @return The geometry
     191     */
     192    public Collection<Geometry> getGeometry() {
     193        return Collections.unmodifiableCollection(this.geometryCollection);
     194    }
     195
     196    /**
     197     * Get a specified key
     198     * @param index The index in the key list
     199     * @return The actual key
     200     */
     201    public String getKey(int index) {
     202        return this.keyList.get(index);
     203    }
     204
     205    /**
     206     * Get the name of the layer
     207     * @return The layer name
     208     */
     209    public String getName() {
     210        return this.name;
     211    }
     212
     213    /**
     214     * Get a specified value
     215     * @param index The index in the value list
     216     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
     217     */
     218    public Object getValue(int index) {
     219        return this.valueList.get(index);
     220    }
     221
     222    /**
     223     * Get the MapBox Vector Tile version specification for this layer
     224     * @return The version of the MapBox Vector Tile specification
     225     */
     226    public byte getVersion() {
     227        return this.version;
     228    }
     229}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.concurrent.ThreadPoolExecutor;
     5
     6import org.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

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.util.Arrays;
     5import java.util.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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
     3
     4import java.awt.Graphics;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.util.Collection;
     8import java.util.HashSet;
     9import java.util.List;
     10import java.util.Objects;
     11import java.util.stream.Collectors;
     12
     13import org.openstreetmap.gui.jmapviewer.Tile;
     14import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     15import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     16import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
     17import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
     18import org.openstreetmap.josm.tools.ListenerList;
     19import org.openstreetmap.josm.tools.Logging;
     20
     21/**
     22 * A class for MapBox Vector Tiles
     23 *
     24 * @author Taylor Smock
     25 * @since xxx
     26 */
     27public class MVTTile extends Tile implements VectorTile {
     28    private final ListenerList<TileListener> listenerList = ListenerList.create();
     29    private Collection<Layer> layers;
     30    private int extent = Layer.DEFAULT_EXTENT;
     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 paint(final Graphics g, final int x, final int y) {
     45        this.paint(g, x, y, 256, 256);
     46    }
     47
     48    @Override
     49    public void loadImage(final InputStream inputStream) throws IOException {
     50        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
     51            this.initLoading();
     52            ProtoBufParser parser = new ProtoBufParser(inputStream);
     53            Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
     54            this.layers = new HashSet<>();
     55            this.layers = protoBufRecords.stream().map(record -> {
     56                Layer mvtLayer = null;
     57                if (record.getField() == Layer.LAYER_FIELD) {
     58                    try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) {
     59                        mvtLayer = new Layer(tParser.allRecords());
     60                    } catch (IOException e) {
     61                        Logging.error(e);
     62                    } finally {
     63                        // Cleanup bytes
     64                        record.close();
     65                    }
     66                }
     67                return mvtLayer;
     68            }).collect(Collectors.toCollection(HashSet::new));
     69            this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
     70            // TODO figure out a better way to free memory
     71            this.finishLoading();
     72            this.listenerList.fireEvent(event -> event.finishedLoading(this));
     73        }
     74    }
     75
     76    @Override
     77    public Collection<Layer> getLayers() {
     78        return this.layers;
     79    }
     80
     81    @Override
     82    public int getExtent() {
     83        return this.extent;
     84    }
     85
     86    /**
     87     * Add a tile loader finisher listener
     88     *
     89     * @param listener The listener to add
     90     */
     91    public void addTileLoaderFinisher(TileListener listener) {
     92        // Add as weak listeners since we don't want to keep unnecessary references.
     93        this.listenerList.addWeakListener(listener);
     94    }
     95
     96    /**
     97     * A class that can be notified that a tile has finished loading
     98     *
     99     * @author Taylor Smock
     100     */
     101    public interface TileListener {
     102        /**
     103         * Called when the MVTTile is finished loading
     104         *
     105         * @param tile The tile that finished loading
     106         */
     107        void finishedLoading(MVTTile tile);
     108    }
     109
     110    /**
     111     * A class used to set the layers that an MVTTile will show.
     112     *
     113     * @author Taylor Smock
     114     */
     115    public interface LayerShower {
     116        /**
     117         * Get a list of layers to show
     118         *
     119         * @return A list of layer names
     120         */
     121        List<String> layersToShow();
     122    }
     123
     124    @Override
     125    public boolean equals(Object obj) {
     126        if (!super.equals(obj) || obj.getClass().isAssignableFrom(this.getClass())) {
     127            return false;
     128        }
     129        MVTTile other = (MVTTile) obj;
     130        return extent == other.extent
     131          && Objects.deepEquals(layers.toArray(), other.layers.toArray());
     132    }
     133
     134    @Override
     135    public int hashCode() {
     136        return Objects.hash(super.hashCode(), extent, layers);
     137    }
     138}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
    new file mode 100644
    - +  
     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.stream.Collectors;
     6
     7import javax.json.JsonArray;
     8import javax.json.JsonObject;
     9import javax.json.JsonString;
     10import javax.json.JsonValue;
     11
     12/**
     13 * A MapBox vector style expression
     14 * @author Taylor Smock
     15 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a>
     16 * @since xxx
     17 */
     18public class Expression {
     19    /** An empty expression to use */
     20    public static final Expression EMPTY_EXPRESSION = new Expression(JsonValue.NULL);
     21    private static final String EMPTY_STRING = "";
     22
     23    private final String expression;
     24
     25    /**
     26     * Create a new filter expression. <i>Please note that this currently only supports basic comparators!</i>
     27     * @param value The value to parse
     28     */
     29    public Expression(JsonValue value) {
     30        if (value.getValueType() == JsonValue.ValueType.ARRAY) {
     31            final JsonArray array = value.asJsonArray();
     32            if (array.get(0).getValueType() == JsonValue.ValueType.STRING) {
     33                if ("==".equals(array.getString(0))) {
     34                    // The mapcss equivalent of == is = (for the most part)
     35                    this.expression = convertToString(array.get(1)) + "=" + convertToString(array.get(2));
     36                } else if (Arrays.asList("<=", ">=", ">", "<", "!=").contains(array.getString(0))) {
     37                    this.expression = convertToString(array.get(1)) + array.getString(0) + convertToString(array.get(2));
     38                } else {
     39                    this.expression = EMPTY_STRING;
     40                }
     41            } else {
     42                this.expression = EMPTY_STRING;
     43            }
     44        } else {
     45            this.expression = EMPTY_STRING;
     46        }
     47    }
     48
     49    /**
     50     * Convert a value to a string
     51     * @param value The value to convert
     52     * @return A string
     53     */
     54    private static String convertToString(JsonValue value) {
     55        switch (value.getValueType()) {
     56        case STRING:
     57            return ((JsonString) value).getString();
     58        case FALSE:
     59            return Boolean.FALSE.toString();
     60        case TRUE:
     61            return Boolean.TRUE.toString();
     62        case NUMBER:
     63            return value.toString();
     64        case ARRAY:
     65            return '['
     66              + ((JsonArray) value).stream().map(Expression::convertToString).collect(Collectors.joining(","))
     67              + ']';
     68        case OBJECT:
     69            return '{'
     70              + ((JsonObject) value).entrySet().stream()
     71              .map(entry -> entry.getKey() + ":" + convertToString(entry.getValue())).collect(
     72                Collectors.joining(","))
     73              + '}';
     74        case NULL:
     75        default:
     76            return EMPTY_STRING;
     77        }
     78    }
     79
     80    @Override
     81    public String toString() {
     82        return !EMPTY_STRING.equals(this.expression) ? '[' + this.expression + ']' : EMPTY_STRING;
     83    }
     84}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java

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

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
    new file mode 100644
    - +  
     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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
     3
     4import java.util.ArrayList;
     5import java.util.Collection;
     6import java.util.Collections;
     7import java.util.List;
     8import java.util.Locale;
     9import java.util.stream.Collectors;
     10
     11import javax.json.JsonArray;
     12import javax.json.JsonObject;
     13import javax.json.JsonString;
     14import javax.json.JsonValue;
     15
     16import org.openstreetmap.josm.data.Bounds;
     17
     18/**
     19 * A source from a MapBox Vector Style
     20 *
     21 * @author Taylor Smock
     22 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a>
     23 * @since xxx
     24 */
     25public class Source {
     26    /**
     27     * WMS servers should contain a "{bbox-epsg-3857}" parameter for the bbox
     28     */
     29    private static final String WMS_BBOX = "bbox-epsg-3857";
     30
     31    /* Common items */
     32    /**
     33     * The name of the source
     34     */
     35    private final String name;
     36    /**
     37     * The type of the source
     38     */
     39    private final SourceType sourceType;
     40
     41    /* Common tiled data */
     42    /**
     43     * The minimum zoom supported
     44     */
     45    private final int minZoom;
     46    /**
     47     * The maximum zoom supported
     48     */
     49    private final int maxZoom;
     50    /**
     51     * The tile urls. These usually have replaceable fields.
     52     */
     53    private final List<String> tileUrls;
     54
     55    /* Vector and raster data */
     56    /**
     57     * The attribution to display for the user
     58     */
     59    private final String attribution;
     60    /**
     61     * The bounds of the data. We should not request data outside of the bounds
     62     */
     63    private final Bounds bounds;
     64    /**
     65     * The property to use as a feature id. Can be parameterized
     66     */
     67    private final String promoteId;
     68    /**
     69     * The tile scheme
     70     */
     71    private final Scheme scheme;
     72    /**
     73     * {@code true} if the tiles should not be cached
     74     */
     75    private final boolean volatileCache;
     76
     77    /* Raster data */
     78    /**
     79     * The tile size
     80     */
     81    private final int tileSize;
     82
     83    /**
     84     * Create a new Source object
     85     *
     86     * @param name The name of the source object
     87     * @param data The data to set the source information with
     88     */
     89    public Source(final String name, final JsonObject data) {
     90        this.name = name;
     91        // "type" is required
     92        final String type = data.getString("type");
     93        this.sourceType = SourceType.valueOf(type.replace("-", "_").toUpperCase(Locale.ROOT));
     94        // This can also contain SourceType.RASTER_DEM (only needs encoding)
     95        if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) {
     96            if (data.containsKey("url")) {
     97                // TODO implement https://github.com/mapbox/tilejson-spec
     98                throw new UnsupportedOperationException();
     99            } else {
     100                this.minZoom = data.getInt("minzoom", 0);
     101                this.maxZoom = data.getInt("maxzoom", 22);
     102                this.attribution = data.getString("attribution", null);
     103                if (data.containsKey("bounds") && data.get("bounds").getValueType() == JsonValue.ValueType.ARRAY) {
     104                    final JsonArray bJsonArray = data.getJsonArray("bounds");
     105                    final double[] bArray = new double[4];
     106                    for (int i = 0; i < 4; i++) {
     107                        bArray[i] = bJsonArray.getJsonNumber(i).doubleValue();
     108                    }
     109                    // The order in the response is
     110                    // [south-west longitude, south-west latitude, north-east longitude, north-east latitude]
     111                    this.bounds = new Bounds(bArray[1], bArray[0], bArray[3], bArray[2]);
     112                } else {
     113                    this.bounds = new Bounds(-85.051129, -180, 85.051129, 180);
     114                }
     115                this.promoteId = data.getString("promoteId", null);
     116                this.scheme = Scheme.valueOf(data.getString("scheme", "xyz").toUpperCase(Locale.ROOT));
     117                if (data.containsKey("tiles") && data.get("tiles").getValueType() == JsonValue.ValueType.ARRAY) {
     118                    this.tileUrls = data.getJsonArray("tiles").stream().filter(JsonString.class::isInstance)
     119                      .map(JsonString.class::cast).map(JsonString::getString)
     120                      // Replace bbox-epsg-3857 with bbox (already encased with {})
     121                      .map(url -> url.replace(WMS_BBOX, "bbox")).collect(Collectors.toList());
     122                } else {
     123                    this.tileUrls = Collections.emptyList();
     124                }
     125                this.volatileCache = data.getBoolean("volatile", false);
     126                this.tileSize = data.getInt("tileSize", 512);
     127            }
     128        } else {
     129            throw new UnsupportedOperationException();
     130        }
     131    }
     132
     133    /**
     134     * Get the source name
     135     * @return the name
     136     */
     137    public String getName() {
     138        return name;
     139    }
     140
     141    /**
     142     * Get the URLs that can be used to get vector data
     143     *
     144     * @return The urls
     145     */
     146    public List<String> getUrls() {
     147        return Collections.unmodifiableList(this.tileUrls);
     148    }
     149
     150    /**
     151     * Get the minimum zoom
     152     *
     153     * @return The min zoom (default {@code 0})
     154     */
     155    public int getMinZoom() {
     156        return this.minZoom;
     157    }
     158
     159    /**
     160     * Get the max zoom
     161     *
     162     * @return The max zoom (default {@code 22})
     163     */
     164    public int getMaxZoom() {
     165        return this.maxZoom;
     166    }
     167
     168    /**
     169     * Get the attribution for this source
     170     *
     171     * @return The attribution text. May be {@code null}.
     172     */
     173    public String getAttributionText() {
     174        return this.attribution;
     175    }
     176
     177    @Override
     178    public String toString() {
     179        Collection<String> parts = new ArrayList<>(1 + this.getUrls().size());
     180        parts.add(this.getName());
     181        parts.addAll(this.getUrls());
     182        return String.join(" ", parts);
     183    }
     184}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
    new file mode 100644
    - +  
     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}
  • new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery.vectortile;
     3
     4import java.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}
  • src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
    a b  
    16371637        RenderBenchmarkCollector benchmark = benchmarkFactory.get();
    16381638        BBox bbox = bounds.toBBox();
    16391639        getSettings(renderVirtualNodes);
    1640 
    16411640        try {
    16421641            if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) {
    16431642                try {
  • src/org/openstreetmap/josm/data/osm/IPrimitive.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/IPrimitive.java b/src/org/openstreetmap/josm/data/osm/IPrimitive.java
    a b  
    369369        return getName();
    370370    }
    371371
     372    /**
     373     * Get an object to synchronize the style cache on. This <i>should</i> be a field that does not change during paint.
     374     * By default, it returns the current object, but should be overriden to avoid some performance issues.
     375     * @return A non-{@code null} object to synchronize on when painting
     376     */
     377    default Object getStyleCacheSyncObject() {
     378        return this;
     379    }
     380
    372381    /**
    373382     * Replies the display name of a primitive formatted by <code>formatter</code>
    374383     * @param formatter formatter to use
  • src/org/openstreetmap/josm/data/osm/IRelationMember.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/IRelationMember.java b/src/org/openstreetmap/josm/data/osm/IRelationMember.java
    a b  
    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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/IWaySegment.java b/src/org/openstreetmap/josm/data/osm/IWaySegment.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.osm;
     3
     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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/RelationMember.java b/src/org/openstreetmap/josm/data/osm/RelationMember.java
    a b  
    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

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7/**
     8 * Parse packed values (only numerical values)
     9 *
     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     * The number of expected values
     39     *
     40     * @return The expected values
     41     */
     42    public int size() {
     43        return this.numbers.length;
     44    }
     45
     46    /**
     47     * Get the parsed number array
     48     *
     49     * @return The number array
     50     */
     51    public Number[] getArray() {
     52        return this.numbers;
     53    }
     54
     55    private byte[] nextVarInt() {
     56        List<Byte> byteList = new ArrayList<>();
     57        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
     58          == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     59            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     60            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     61        }
     62        // The last byte doesn't drop the most significant bit
     63        byteList.add(this.bytes[this.location++]);
     64        byte[] byteArray = new byte[byteList.size()];
     65        for (int i = 0; i < byteList.size(); i++) {
     66            byteArray[i] = byteList.get(i);
     67        }
     68
     69        return byteArray;
     70    }
     71}
  • new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.IOException;
     5import java.nio.charset.StandardCharsets;
     6import java.util.stream.Stream;
     7
     8import org.openstreetmap.josm.tools.Utils;
     9
     10/**
     11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
     12 *
     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 the field value
     52     *
     53     * @return The field value
     54     */
     55    public int getField() {
     56        return this.field;
     57    }
     58
     59    /**
     60     * Get the WireType of the data
     61     *
     62     * @return The {@link WireType} of the data
     63     */
     64    public WireType getType() {
     65        return this.type;
     66    }
     67
     68    /**
     69     * Get the raw bytes for this record
     70     *
     71     * @return The bytes
     72     */
     73    public byte[] getBytes() {
     74        return this.bytes;
     75    }
     76
     77    /**
     78     * Get the var int ({@code WireType#VARINT})
     79     *
     80     * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     81     */
     82    public Number asUnsignedVarInt() {
     83        return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
     84    }
     85
     86    /**
     87     * Get the signed var int ({@code WireType#VARINT}).
     88     * These are specially encoded so that they take up less space.
     89     *
     90     * @return The signed var int ({@code sint32} or {@code sint64})
     91     */
     92    public Number asSignedVarInt() {
     93        final Number signed = this.asUnsignedVarInt();
     94        return ProtoBufParser.decodeZigZag(signed);
     95    }
     96
     97    /**
     98     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     99     *
     100     * @return the double
     101     */
     102    public double asDouble() {
     103        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     104        return Double.longBitsToDouble(doubleNumber);
     105    }
     106
     107    /**
     108     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     109     *
     110     * @return the float
     111     */
     112    public float asFloat() {
     113        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     114        return Float.intBitsToFloat(floatNumber);
     115    }
     116
     117    /**
     118     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     119     *
     120     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     121     */
     122    public String asString() {
     123        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     124    }
     125
     126    /**
     127     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     128     *
     129     * @return a byte array of the 32 bits (4 bytes)
     130     */
     131    public byte[] asFixed32() {
     132        // TODO verify, or just assume?
     133        // 4 bytes == 32 bits
     134        return this.bytes;
     135    }
     136
     137    /**
     138     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     139     *
     140     * @return a byte array of the 64 bits (8 bytes)
     141     */
     142    public byte[] asFixed64() {
     143        // TODO verify, or just assume?
     144        // 8 bytes == 64 bits
     145        return this.bytes;
     146    }
     147
     148    @Override
     149    public void close() {
     150        this.bytes = null;
     151    }
     152}
  • new file src/org/openstreetmap/josm/data/protobuf/WireType.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4/**
     5 * The WireTypes
     6 *
     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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/DataLayer.java b/src/org/openstreetmap/josm/data/vector/DataLayer.java
    new file mode 100644
    - +  
     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/VectorDataSet.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
    new file mode 100644
    - +  
     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.Arrays;
     10import java.util.Collection;
     11import java.util.Collections;
     12import java.util.HashSet;
     13import java.util.LinkedList;
     14import java.util.List;
     15import java.util.Map;
     16import java.util.Objects;
     17import java.util.Optional;
     18import java.util.Set;
     19import java.util.concurrent.ConcurrentHashMap;
     20import java.util.concurrent.locks.Lock;
     21import java.util.concurrent.locks.ReentrantReadWriteLock;
     22import java.util.function.Predicate;
     23import java.util.function.Supplier;
     24import java.util.stream.Collectors;
     25import java.util.stream.Stream;
     26
     27import org.openstreetmap.gui.jmapviewer.Coordinate;
     28import org.openstreetmap.gui.jmapviewer.Tile;
     29import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
     30import org.openstreetmap.josm.data.DataSource;
     31import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
     32import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     33import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     34import org.openstreetmap.josm.data.osm.BBox;
     35import org.openstreetmap.josm.data.osm.DataSelectionListener;
     36import org.openstreetmap.josm.data.osm.DownloadPolicy;
     37import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     38import org.openstreetmap.josm.data.osm.INode;
     39import org.openstreetmap.josm.data.osm.IPrimitive;
     40import org.openstreetmap.josm.data.osm.IRelation;
     41import org.openstreetmap.josm.data.osm.IWay;
     42import org.openstreetmap.josm.data.osm.OsmData;
     43import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     44import org.openstreetmap.josm.data.osm.PrimitiveId;
     45import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore;
     46import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
     47import org.openstreetmap.josm.data.osm.Storage;
     48import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
     49import org.openstreetmap.josm.data.osm.UploadPolicy;
     50import org.openstreetmap.josm.data.osm.WaySegment;
     51import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
     52import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     53import org.openstreetmap.josm.tools.Geometry;
     54import org.openstreetmap.josm.tools.ListenerList;
     55import org.openstreetmap.josm.tools.Logging;
     56import org.openstreetmap.josm.tools.SubclassFilteredCollection;
     57
     58/**
     59 * A data class for Vector Data
     60 *
     61 * @author Taylor Smock
     62 * @since xxx
     63 */
     64public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
     65    // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
     66    // for new values (perf increase). See JDK-8161372 for more info.
     67    private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>();
     68    private final Collection<PrimitiveId> selected = new HashSet<>();
     69    // Both of these listener lists are useless, since they expect OsmPrimitives at this time
     70    private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
     71    private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
     72    private boolean lock = true;
     73    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
     74    private String name;
     75    private short mappaintCacheIdx = 1;
     76
     77    /**
     78     * The distance to consider nodes duplicates -- mostly a memory saving measure.
     79     * 0.000_000_1 ~1.2 cm (+- 5.57 mm)
     80     * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a>
     81     * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate
     82     * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision)
     83     */
     84    protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f;
     85
     86    /**
     87     * The current zoom we are getting/adding to
     88     */
     89    private int zoom;
     90    /**
     91     * Default to normal download policy
     92     */
     93    private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL;
     94    /**
     95     * Default to a blocked upload policy
     96     */
     97    private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED;
     98    /**
     99     * The paint style for this layer
     100     */
     101    private ElemStyles styles;
     102
     103    @Override public Collection<DataSource> getDataSources() {
     104        final int currentZoom = this.zoom;
     105        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     106        return dataStore.getDataSources();
     107    }
     108
     109    /**
     110     * Add a data source
     111     *
     112     * @param currentZoom the zoom
     113     * @param dataSource  The datasource to add at the zoom level
     114     */
     115    public void addDataSource(int currentZoom, DataSource dataSource) {
     116        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     117        dataStore.addDataSource(dataSource);
     118    }
     119
     120    @Override public void lock() {
     121        this.lock = true;
     122    }
     123
     124    @Override public void unlock() {
     125        this.lock = false;
     126    }
     127
     128    @Override public boolean isLocked() {
     129        return this.lock;
     130    }
     131
     132    @Override public String getVersion() {
     133        return "8"; // TODO
     134    }
     135
     136    @Override public String getName() {
     137        return this.name;
     138    }
     139
     140    @Override public void setName(String name) {
     141        this.name = name;
     142    }
     143
     144    @Override public void addPrimitive(VectorPrimitive primitive) {
     145        primitive.setDataSet(this);
     146        final int currentZoom = this.zoom;
     147        this.tryWrite(() -> {
     148            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     149            dataStore.addPrimitive(primitive);
     150        });
     151    }
     152
     153    /**
     154     * Remove a primitive from this dataset
     155     *
     156     * @param primitive The primitive to remove
     157     */
     158    protected void removePrimitive(VectorPrimitive primitive) {
     159        if (primitive.getDataSet() == this) {
     160            final int currentZoom = this.zoom;
     161            primitive.setDataSet(null);
     162            this.tryWrite(() -> {
     163                final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     164                dataStore.removePrimitive(primitive);
     165            });
     166        }
     167    }
     168
     169    @Override public void clear() {
     170        this.tryWrite(this.dataStoreMap::clear);
     171    }
     172
     173    @Override public List<VectorNode> searchNodes(BBox bbox) {
     174        return this.tryRead(() -> {
     175            final int currentZoom = this.zoom;
     176            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     177            return dataStore.getStore().searchNodes(bbox);
     178        }).orElseGet(Collections::emptyList);
     179    }
     180
     181    @Override public boolean containsNode(VectorNode vectorNode) {
     182        return this.tryRead(() -> {
     183            final int currentZoom = this.zoom;
     184            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     185            return dataStore.getStore().containsNode(vectorNode);
     186        }).orElse(false);
     187    }
     188
     189    @Override public List<VectorWay> searchWays(BBox bbox) {
     190        return this.tryRead(() -> {
     191            final int currentZoom = this.zoom;
     192            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     193            return dataStore.getStore().searchWays(bbox);
     194        }).orElseGet(Collections::emptyList);
     195    }
     196
     197    @Override public boolean containsWay(VectorWay vectorWay) {
     198        return this.tryRead(() -> {
     199            final int currentZoom = this.zoom;
     200            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     201            return dataStore.getStore().containsWay(vectorWay);
     202        }).orElse(false);
     203    }
     204
     205    @Override public List<VectorRelation> searchRelations(BBox bbox) {
     206        return this.tryRead(() -> {
     207            final int currentZoom = this.zoom;
     208            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     209            return dataStore.getStore().searchRelations(bbox);
     210        }).orElseGet(Collections::emptyList);
     211    }
     212
     213    @Override public boolean containsRelation(VectorRelation vectorRelation) {
     214        return this.tryRead(() -> {
     215            final int currentZoom = this.zoom;
     216            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     217            return dataStore.getStore().containsRelation(vectorRelation);
     218        }).orElse(false);
     219    }
     220
     221    @Override public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
     222        return this.tryRead(() -> {
     223            final int currentZoom = this.zoom;
     224            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     225            return dataStore.getPrimitivesMap().get(primitiveId);
     226        }).orElse(null);
     227    }
     228
     229    @Override public <T extends VectorPrimitive> Collection<T> getPrimitives(
     230      Predicate<? super VectorPrimitive> predicate) {
     231        // index should be negative, as the datastore doesn't have that zoom level
     232        // Prefer the higher zoom level
     233        Optional<Collection<T>> optional = this.tryRead(() -> {
     234            int currentZoom = this.zoom;
     235            if (!this.dataStoreMap.containsKey(currentZoom)) {
     236                // index should be negative, as the datastore doesn't have that zoom level
     237                final int[] keys = this.dataStoreMap.keySet().stream().mapToInt(Integer::intValue).sorted().toArray();
     238                final int index = 1 - Arrays.binarySearch(keys, currentZoom);
     239                // Prefer the higher zoom level
     240                if (index < keys.length - 2) {
     241                    currentZoom = keys[index + 1];
     242                } else if (index > 0 && keys.length > index - 1) {
     243                    currentZoom = keys[index - 1];
     244                }
     245            }
     246            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     247            return new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate);
     248        });
     249        return optional.orElseGet(Collections::emptyList);
     250    }
     251
     252    @Override public Collection<VectorNode> getNodes() {
     253        return this.getPrimitives(VectorNode.class::isInstance);
     254    }
     255
     256    @Override public Collection<VectorWay> getWays() {
     257        return this.getPrimitives(VectorWay.class::isInstance);
     258    }
     259
     260    @Override public Collection<VectorRelation> getRelations() {
     261        return this.getPrimitives(VectorRelation.class::isInstance);
     262    }
     263
     264    @Override public DownloadPolicy getDownloadPolicy() {
     265        return this.downloadPolicy;
     266    }
     267
     268    @Override public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
     269        this.downloadPolicy = downloadPolicy;
     270    }
     271
     272    @Override public UploadPolicy getUploadPolicy() {
     273        return this.uploadPolicy;
     274    }
     275
     276    @Override public void setUploadPolicy(UploadPolicy uploadPolicy) {
     277        this.uploadPolicy = uploadPolicy;
     278    }
     279
     280    @Override public Lock getReadLock() {
     281        return this.readWriteLock.readLock();
     282    }
     283
     284    @Override public Collection<WaySegment> getHighlightedVirtualNodes() {
     285        // TODO?
     286        return Collections.emptyList();
     287    }
     288
     289    @Override public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
     290        // TODO?
     291    }
     292
     293    @Override public Collection<WaySegment> getHighlightedWaySegments() {
     294        // TODO?
     295        return Collections.emptyList();
     296    }
     297
     298    @Override public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
     299        // TODO?
     300    }
     301
     302    @Override public void addHighlightUpdateListener(HighlightUpdateListener listener) {
     303        this.highlightUpdateListenerListenerList.addListener(listener);
     304    }
     305
     306    @Override public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
     307        this.highlightUpdateListenerListenerList.removeListener(listener);
     308    }
     309
     310    @Override public Collection<VectorPrimitive> getAllSelected() {
     311        final int currentZoom = this.zoom;
     312        final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new);
     313        return dataStore.getAllPrimitives().stream()
     314          .filter(primitive -> this.selected.contains(primitive.getPrimitiveId())).collect(
     315            Collectors.toList());
     316    }
     317
     318    @Override public boolean selectionEmpty() {
     319        return this.selected.isEmpty();
     320    }
     321
     322    @Override public boolean isSelected(VectorPrimitive osm) {
     323        return this.selected.contains(osm.getPrimitiveId());
     324    }
     325
     326    @Override public void toggleSelected(Collection<? extends PrimitiveId> osm) {
     327        this.toggleSelectedImpl(osm.stream());
     328    }
     329
     330    @Override public void toggleSelected(PrimitiveId... osm) {
     331        this.toggleSelectedImpl(Stream.of(osm));
     332    }
     333
     334    private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
     335        osm.forEach(primitiveId -> {
     336            if (this.selected.contains(primitiveId)) {
     337                this.selected.remove(primitiveId);
     338            } else {
     339                this.selected.add(primitiveId);
     340            }
     341        });
     342    }
     343
     344    @Override public void setSelected(Collection<? extends PrimitiveId> selection) {
     345        this.setSelectedImpl(selection.stream());
     346    }
     347
     348    @Override public void setSelected(PrimitiveId... osm) {
     349        this.setSelectedImpl(Stream.of(osm));
     350    }
     351
     352    private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
     353        this.selected.clear();
     354        osm.forEach(this.selected::add);
     355    }
     356
     357    @Override public void addSelected(Collection<? extends PrimitiveId> selection) {
     358        this.addSelectedImpl(selection.stream());
     359    }
     360
     361    @Override public void addSelected(PrimitiveId... osm) {
     362        this.addSelectedImpl(Stream.of(osm));
     363    }
     364
     365    private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
     366        osm.forEach(this.selected::add);
     367    }
     368
     369    @Override public void clearSelection(PrimitiveId... osm) {
     370        this.clearSelectionImpl(Stream.of(osm));
     371    }
     372
     373    @Override public void clearSelection(Collection<? extends PrimitiveId> list) {
     374        this.clearSelectionImpl(list.stream());
     375    }
     376
     377    @Override public void clearSelection() {
     378        this.clearSelectionImpl(new ArrayList<>(this.selected).stream());
     379    }
     380
     381    private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
     382        osm.forEach(this.selected::remove);
     383    }
     384
     385    @Override public void addSelectionListener(DataSelectionListener listener) {
     386        this.dataSelectionListenerListenerList.addListener(listener);
     387    }
     388
     389    @Override public void removeSelectionListener(DataSelectionListener listener) {
     390        this.dataSelectionListenerListenerList.removeListener(listener);
     391    }
     392
     393    public short getMappaintCacheIndex() {
     394        return this.mappaintCacheIdx;
     395    }
     396
     397    @Override public void clearMappaintCache() {
     398        this.mappaintCacheIdx++;
     399    }
     400
     401    public void setZoom(int zoom) {
     402        if (zoom == this.zoom) {
     403            return; // Do nothing -- zoom isn't actually changing
     404        }
     405        this.tryWrite(() -> {
     406            this.zoom = zoom;
     407            this.clearMappaintCache();
     408            final int[] nearestZoom = new int[] {-1, -1, -1};
     409            nearestZoom[0] = zoom;
     410            // Create a new list to avoid concurrent modification issues
     411            final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull)
     412              .mapToInt(Integer::intValue).sorted().toArray();
     413            final int index;
     414            if (this.dataStoreMap.containsKey(zoom)) {
     415                index = Arrays.binarySearch(keys, zoom);
     416            } else {
     417                // (-(insertion point) - 1) = return -> insertion point = -(return + 1)
     418                index = -(Arrays.binarySearch(keys, zoom) + 1);
     419            }
     420            if (index > 0) {
     421                nearestZoom[1] = keys[index - 1];
     422            }
     423            if (index < keys.length - 2) {
     424                nearestZoom[2] = keys[index + 1];
     425            }
     426            // Clear zoom levels not immediately above/below the current zoom level (attempt to save some memory)
     427            //IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key)).forEach(this.dataStoreMap::remove);
     428        });
     429    }
     430
     431    public int getZoom() {
     432        return this.zoom;
     433    }
     434
     435    /**
     436     * Add tile data to this dataset
     437     * @param tile The tile to add
     438     * @param <T> The tile type
     439     */
     440    public <T extends Tile & VectorTile> void addTileData(T tile) {
     441        this.tryWrite(() -> {
     442            final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(tile.getZoom(), VectorDataStore::new);
     443            dataStore.addTile(tile);
     444        });
     445    }
     446
     447    /**
     448     * Try to read something (here to avoid boilerplate)
     449     *
     450     * @param supplier The reading function
     451     * @param <T>      The return type
     452     * @return The optional return
     453     */
     454    private <T> Optional<T> tryRead(Supplier<T> supplier) {
     455        try {
     456            this.readWriteLock.readLock().lockInterruptibly();
     457            return Optional.ofNullable(supplier.get());
     458        } catch (InterruptedException e) {
     459            Logging.error(e);
     460            Thread.currentThread().interrupt();
     461        } finally {
     462            this.readWriteLock.readLock().unlock();
     463        }
     464        return Optional.empty();
     465    }
     466
     467    /**
     468     * Try to write something (here to avoid boilerplate)
     469     *
     470     * @param runnable The writing function
     471     */
     472    private void tryWrite(Runnable runnable) {
     473        try {
     474            this.readWriteLock.writeLock().lockInterruptibly();
     475            runnable.run();
     476        } catch (InterruptedException e) {
     477            Logging.error(e);
     478            Thread.currentThread().interrupt();
     479        } finally {
     480            if (this.readWriteLock.isWriteLockedByCurrentThread()) {
     481                this.readWriteLock.writeLock().unlock();
     482            }
     483        }
     484    }
     485
     486    /**
     487     * Get the styles for this layer
     488     *
     489     * @return The styles
     490     */
     491    public ElemStyles getStyles() {
     492        return this.styles;
     493    }
     494
     495    /**
     496     * Set the styles for this layer
     497     * @param styles The styles to set for this layer
     498     */
     499    public void setStyles(Collection<ElemStyles> styles) {
     500        if (styles.size() == 1) {
     501            this.styles = styles.iterator().next();
     502        } else if (!styles.isEmpty()) {
     503            this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList()));
     504        } else {
     505            this.styles = null;
     506        }
     507    }
     508
     509    /**
     510     * This literally only exists to make {@link QuadBucketPrimitiveStore#removePrimitive} public
     511     *
     512     * @param <N> The node type
     513     * @param <W> The way type
     514     * @param <R> The relation type
     515     */
     516    private static class LocalQuadBucketPrimitiveStore<N extends INode, W extends IWay<N>, R extends IRelation<?>>
     517      extends QuadBucketPrimitiveStore<N, W, R> {
     518        // Allow us to remove primitives
     519        @Override
     520        public void removePrimitive(IPrimitive primitive) {
     521            if ((primitive instanceof IRelation && this.containsRelation((R) primitive))
     522                || (primitive instanceof IWay && this.containsWay((W) primitive))
     523                || (primitive instanceof INode && this.containsNode((N) primitive))) {
     524                super.removePrimitive(primitive);
     525            }
     526        }
     527    }
     528
     529    private static class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> {
     530        protected final int zoom;
     531        protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
     532        protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
     533        protected final Set<Tile> addedTiles = new HashSet<>();
     534        protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
     535          .foreignKey(new Storage.PrimitiveIdHash());
     536        protected final Collection<DataSource> dataSources = new LinkedList<>();
     537
     538        DataStore(int zoom) {
     539            this.zoom = zoom;
     540        }
     541
     542        public int getZoom() {
     543            return this.zoom;
     544        }
     545
     546        public QuadBucketPrimitiveStore<N, W, R> getStore() {
     547            return this.store;
     548        }
     549
     550        public Storage<O> getAllPrimitives() {
     551            return this.allPrimitives;
     552        }
     553
     554        public Map<PrimitiveId, O> getPrimitivesMap() {
     555            return this.primitivesMap;
     556        }
     557
     558        public Collection<DataSource> getDataSources() {
     559            return Collections.unmodifiableCollection(dataSources);
     560        }
     561
     562        /**
     563         * Add a datasource to this data set
     564         * @param dataSource The data soure to add
     565         */
     566        public void addDataSource(DataSource dataSource) {
     567            this.dataSources.add(dataSource);
     568        }
     569
     570        /**
     571         * Add a primitive to this dataset
     572         * @param primitive The primitive to remove
     573         */
     574        protected void removePrimitive(O primitive) {
     575            this.store.removePrimitive(primitive);
     576            this.allPrimitives.remove(primitive);
     577            this.primitivesMap.remove(primitive.getPrimitiveId());
     578        }
     579
     580        /**
     581         * Add a primitive to this dataset
     582         * @param primitive The primitive to add
     583         */
     584        protected void addPrimitive(O primitive) {
     585            this.store.addPrimitive(primitive);
     586            this.allPrimitives.add(primitive);
     587            this.primitivesMap.put(primitive.getPrimitiveId(), primitive);
     588        }
     589    }
     590
     591    private class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
     592        private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
     593
     594        VectorDataStore(int zoom) {
     595            super(zoom);
     596        }
     597
     598        @Override
     599        protected void addPrimitive(VectorPrimitive primitive) {
     600            primitive.setDataSet(VectorDataSet.this);
     601            // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways)
     602            if (primitive.getUniqueId() == 0) {
     603                final UniqueIdGenerator generator = primitive.getIdGenerator();
     604                long id;
     605                do {
     606                    id = generator.generateUniqueId();
     607                } while (this.primitivesMap.containsKey(new SimplePrimitiveId(id, primitive.getType())));
     608                primitive.setId(primitive.getIdGenerator().generateUniqueId());
     609            }
     610            if (primitive instanceof VectorRelation && !primitive.isMultipolygon()) {
     611                primitive = mergeWays((VectorRelation) primitive);
     612            }
     613            final VectorPrimitive alreadyAdded = this.primitivesMap.get(primitive.getPrimitiveId());
     614            final VectorRelation mergedRelation = (VectorRelation) this.primitivesMap
     615              .get(new SimplePrimitiveId(primitive.getPrimitiveId().getUniqueId(),
     616                OsmPrimitiveType.RELATION));
     617            if (alreadyAdded == null || alreadyAdded.equals(primitive)) {
     618                super.addPrimitive(primitive);
     619            } else if (mergedRelation != null && mergedRelation.get(JOSM_MERGE_TYPE_KEY) != null) {
     620                mergedRelation.addRelationMember(new VectorRelationMember("", primitive));
     621                super.addPrimitive(primitive);
     622                // Check that all primitives can be merged
     623                if (mergedRelation.getMemberPrimitivesList().stream().allMatch(IWay.class::isInstance)) {
     624                    // This pretty much does the "right" thing
     625                    this.mergeWays(mergedRelation);
     626                } else if (!(primitive instanceof IWay)) {
     627                    // Can't merge, ever (one of the childs is a node/relation)
     628                    mergedRelation.remove(JOSM_MERGE_TYPE_KEY);
     629                }
     630            } else if (mergedRelation != null && primitive instanceof IRelation) {
     631                // Just add to the relation
     632                ((VectorRelation) primitive).getMembers().forEach(mergedRelation::addRelationMember);
     633            } else if (alreadyAdded instanceof VectorWay && primitive instanceof VectorWay) {
     634                final VectorRelation temporaryRelation =
     635                  mergedRelation == null ? new VectorRelation(primitive.getLayer()) : mergedRelation;
     636                if (mergedRelation == null) {
     637                    temporaryRelation.put(JOSM_MERGE_TYPE_KEY, "merge");
     638                    temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded));
     639                }
     640                temporaryRelation.addRelationMember(new VectorRelationMember("", primitive));
     641                temporaryRelation.setDataSet(VectorDataSet.this);
     642                super.addPrimitive(primitive);
     643                super.addPrimitive(temporaryRelation);
     644            }
     645        }
     646
     647        private VectorPrimitive mergeWays(VectorRelation relation) {
     648            List<VectorRelationMember> members = RelationSorter.sortMembersByConnectivity(relation.getMembers());
     649            Collection<VectorWay> relationWayList = relation.getMemberPrimitivesList().stream()
     650              .filter(VectorWay.class::isInstance)
     651              .map(VectorWay.class::cast).collect(Collectors.toCollection(ArrayList::new));
     652            // Only support way-only relations
     653            if (relationWayList.size() != relation.getMemberPrimitivesList().size()) {
     654                return relation;
     655            }
     656            List<VectorWay> wayList = new ArrayList<>(relation.getMembersCount());
     657            // Assume that the order may not be correct, worst case O(n), best case O(n/2)
     658            // Assume that the ways were drawn in order
     659            final int maxIteration = relationWayList.size();
     660            int iteration = 0;
     661            while (iteration < maxIteration && wayList.size() < relationWayList.size()) {
     662                for (VectorWay way : relationWayList) {
     663                    if (wayList.isEmpty()) {
     664                        wayList.add(way);
     665                        continue;
     666                    }
     667                    // Check first/last ways
     668                    if (canMergeWays(wayList.get(0), way, false)) {
     669                        wayList.add(0, way);
     670                    } else if (canMergeWays(wayList.get(wayList.size() - 1), way, false)) {
     671                        wayList.add(way);
     672                    }
     673                }
     674                iteration++;
     675                relationWayList.removeIf(wayList::contains);
     676            }
     677            if (!relationWayList.isEmpty()) {
     678                return relation;
     679            }
     680            // Merge ways
     681            List<VectorNode> nodes = new ArrayList<>();
     682            for (VectorWay way : wayList) {
     683                for (VectorNode node : way.getNodes()) {
     684                    if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) {
     685                        nodes.add(node);
     686                    }
     687                }
     688            }
     689            VectorWay way = wayList.get(0);
     690            way.setNodes(nodes);
     691            wayList.remove(way);
     692            wayList.forEach(this::removePrimitive);
     693            this.removePrimitive(relation);
     694            return way;
     695        }
     696
     697        private <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) {
     698            final List<N> nodes = new ArrayList<>(old.getNodes());
     699            boolean added = true;
     700            if (allowReverse && old.firstNode().equals(toAdd.firstNode())) {
     701                // old <-|-> new becomes old ->|-> new
     702                Collections.reverse(nodes);
     703                nodes.addAll(toAdd.getNodes());
     704            } else if (old.firstNode().equals(toAdd.lastNode())) {
     705                // old <-|<- new, so we prepend the new nodes in order
     706                nodes.addAll(0, toAdd.getNodes());
     707            } else if (old.lastNode().equals(toAdd.firstNode())) {
     708                // old ->|-> new, we just add it
     709                nodes.addAll(toAdd.getNodes());
     710            } else if (allowReverse && old.lastNode().equals(toAdd.lastNode())) {
     711                // old ->|<- new, we need to reverse new
     712                final List<N> toAddNodes = new ArrayList<>(toAdd.getNodes());
     713                Collections.reverse(toAddNodes);
     714                nodes.addAll(toAddNodes);
     715            } else {
     716                added = false;
     717            }
     718            if (added) {
     719                // This is (technically) always correct
     720                old.setNodes(nodes);
     721            }
     722            return added;
     723        }
     724
     725        private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer,
     726          Collection<VectorPrimitive> featureObjects, int x, int y) {
     727            final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
     728            final int layerExtent = layer.getExtent() * 2;
     729            final ICoordinate lowerRight = tile.getTileSource()
     730              .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     731            final ICoordinate coords = new Coordinate(
     732              upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent,
     733              upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent);
     734            final Collection<VectorNode> nodes = this.store
     735              .searchNodes(new BBox(coords.getLon(), coords.getLat(), DUPE_NODE_DISTANCE));
     736            final VectorNode node;
     737            if (!nodes.isEmpty()) {
     738                final VectorNode first = nodes.iterator().next();
     739                if (first.isDisabled() || !first.isVisible()) {
     740                    // Only replace nodes that are not visible
     741                    node = new VectorNode(layer.getName());
     742                    node.setCoor(node.getCoor());
     743                    first.getReferrers(true).forEach(primitive -> {
     744                        if (primitive instanceof VectorWay) {
     745                            List<VectorNode> nodeList = new ArrayList<>(((VectorWay) primitive).getNodes());
     746                            nodeList.replaceAll(vnode -> vnode.equals(first) ? node : vnode);
     747                            ((VectorWay) primitive).setNodes(nodeList);
     748                        } else if (primitive instanceof VectorRelation) {
     749                            List<VectorRelationMember> members = new ArrayList<>(((VectorRelation) primitive).getMembers());
     750                            members.replaceAll(member ->
     751                              member.getMember().equals(first) ? new VectorRelationMember(member.getRole(), node): member);
     752                            ((VectorRelation) primitive).setMembers(members);
     753                        }
     754                    });
     755                    this.removePrimitive(first);
     756                } else {
     757                    node = first;
     758                }
     759            } else {
     760                node = new VectorNode(layer.getName());
     761            }
     762            node.setCoor(coords);
     763            featureObjects.add(node);
     764            return node;
     765        }
     766
     767        private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer,
     768          Collection<VectorPrimitive> featureObjects, Path2D shape) {
     769            final PathIterator pathIterator = shape.getPathIterator(null);
     770            final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream()
     771              .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(
     772                Collectors.toList());
     773            // These nodes technically do not exist, so we shouldn't show them
     774            ways.stream().flatMap(way -> way.getNodes().stream())
     775              .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0)
     776              .forEach(prim -> {
     777                  prim.setDisabled(true);
     778                  prim.setVisible(false);
     779              });
     780            return ways;
     781        }
     782
     783        private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer,
     784          Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) {
     785            final List<VectorNode> nodes = new ArrayList<>();
     786            final double[] coords = new double[6];
     787            final List<VectorPrimitive> ways = new ArrayList<>();
     788            do {
     789                final int type = pathIterator.currentSegment(coords);
     790                pathIterator.next();
     791                if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) {
     792                    if (PathIterator.SEG_CLOSE == type) {
     793                        nodes.add(nodes.get(0));
     794                    }
     795                    // New line
     796                    if (!nodes.isEmpty()) {
     797                        final VectorWay way = new VectorWay(layer.getName());
     798                        way.setNodes(nodes);
     799                        featureObjects.add(way);
     800                        ways.add(way);
     801                    }
     802                    nodes.clear();
     803                }
     804                if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) {
     805                    final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]);
     806                    nodes.add(node);
     807                } else if (PathIterator.SEG_CLOSE != type) {
     808                    // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time.
     809                    throw new UnsupportedOperationException();
     810                }
     811            } while (!pathIterator.isDone());
     812            if (!nodes.isEmpty()) {
     813                final VectorWay way = new VectorWay(layer.getName());
     814                way.setNodes(nodes);
     815                featureObjects.add(way);
     816                ways.add(way);
     817            }
     818            return ways;
     819        }
     820
     821        private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer,
     822          Collection<VectorPrimitive> featureObjects, Area area) {
     823            final PathIterator pathIterator = area.getPathIterator(null);
     824            final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator);
     825            VectorRelation vectorRelation = new VectorRelation(layer.getName());
     826            for (VectorPrimitive member : members) {
     827                final String role;
     828                if (member instanceof VectorWay && ((VectorWay) member).isClosed()) {
     829                    role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner";
     830                } else {
     831                    role = "";
     832                }
     833                vectorRelation.addRelationMember(new VectorRelationMember(role, member));
     834            }
     835            return vectorRelation;
     836        }
     837
     838        /**
     839         * Add a tile to this data store
     840         * @param tile The tile to add
     841         * @param <T> The tile type
     842         */
     843        public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
     844            Optional<Tile> previous = this.addedTiles.stream()
     845              .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
     846            // Check if we have already added the tile (just to save processing time)
     847            if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
     848                previous.ifPresent(this.addedTiles::remove);
     849                this.addedTiles.add(tile);
     850                for (Layer layer : tile.getLayers()) {
     851                    layer.getGeometry().forEach(geometry -> {
     852                        List<VectorPrimitive> featureObjects = new ArrayList<>();
     853                        List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
     854                        geometry.getShapes().forEach(shape -> {
     855                            final VectorPrimitive primitive;
     856                            if (shape instanceof Ellipse2D) {
     857                                primitive = pointToNode(tile, layer, featureObjects,
     858                                  (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
     859                            } else if (shape instanceof Path2D) {
     860                                primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst()
     861                                  .orElse(null);
     862                            } else if (shape instanceof Area) {
     863                                primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
     864                                primitive.put("type", "multipolygon");
     865                            } else {
     866                                // We shouldn't hit this, but just in case
     867                                throw new UnsupportedOperationException();
     868                            }
     869                            primaryFeatureObjects.add(primitive);
     870                        });
     871                        final VectorPrimitive primitive;
     872                        if (primaryFeatureObjects.size() == 1) {
     873                            primitive = primaryFeatureObjects.get(0);
     874                            if (primitive instanceof IRelation && !primitive.isMultipolygon()) {
     875                                primitive.put(JOSM_MERGE_TYPE_KEY, "merge");
     876                            }
     877                        } else if (!primaryFeatureObjects.isEmpty()) {
     878                            VectorRelation relation = new VectorRelation(layer.getName());
     879                            primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim))
     880                              .forEach(relation::addRelationMember);
     881                            primitive = relation;
     882                        } else {
     883                            return;
     884                        }
     885                        Feature feature = geometry.getFeature();
     886                        primitive.setId(feature.getId());
     887                        feature.getTags().forEach(primitive::put);
     888                        featureObjects.forEach(this::addPrimitive);
     889                        primaryFeatureObjects.forEach(this::addPrimitive);
     890                        this.addPrimitive(primitive);
     891                    });
     892                }
     893            }
     894        }
     895    }
     896}
  • new file src/org/openstreetmap/josm/data/vector/VectorNode.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java
    new file mode 100644
    - +  
     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 public double lon() {
     37        return this.lon;
     38    }
     39
     40    @Override public double lat() {
     41        return this.lat;
     42    }
     43
     44    @Override public UniqueIdGenerator getIdGenerator() {
     45        return ID_GENERATOR;
     46    }
     47
     48    @Override public LatLon getCoor() {
     49        return new LatLon(this.lat, this.lon);
     50    }
     51
     52    @Override public void setCoor(LatLon coordinates) {
     53        this.lat = coordinates.lat();
     54        this.lon = coordinates.lon();
     55    }
     56
     57    /**
     58     * Set the coordinates of this node
     59     *
     60     * @param coordinates The coordinates to set
     61     * @see #setCoor(LatLon)
     62     */
     63    public void setCoor(ICoordinate coordinates) {
     64        this.lat = coordinates.getLat();
     65        this.lon = coordinates.getLon();
     66    }
     67
     68    @Override public void setEastNorth(EastNorth eastNorth) {
     69        final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth);
     70        this.lat = ll.lat();
     71        this.lon = ll.lon();
     72    }
     73
     74    @Override public boolean isReferredByWays(int n) {
     75        // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
     76        // when way is cloned
     77        List<? extends IPrimitive> referrers = super.getReferrers();
     78        if (referrers == null || referrers.isEmpty())
     79            return false;
     80        if (referrers instanceof IPrimitive)
     81            return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet();
     82        else {
     83            int counter = 0;
     84            for (IPrimitive o : referrers) {
     85                if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n)
     86                    return true;
     87            }
     88            return false;
     89        }
     90    }
     91
     92    @Override public void accept(PrimitiveVisitor visitor) {
     93        visitor.visit(this);
     94    }
     95
     96    @Override public BBox getBBox() {
     97        return new BBox(this.lon, this.lat);
     98    }
     99
     100    @Override public OsmPrimitiveType getType() {
     101        return OsmPrimitiveType.NODE;
     102    }
     103}
  • new file src/org/openstreetmap/josm/data/vector/VectorPrimitive.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
    new file mode 100644
    - +  
     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 public UniqueIdGenerator getIdGenerator() {
     35        return RELATION_ID_GENERATOR;
     36    }
     37
     38    @Override public void accept(PrimitiveVisitor visitor) {
     39        visitor.visit(this);
     40    }
     41
     42    @Override public BBox getBBox() {
     43        if (cachedBBox == null) {
     44            cachedBBox = new BBox();
     45            for (IPrimitive member : this.getMemberPrimitivesList()) {
     46                cachedBBox.add(member.getBBox());
     47            }
     48        }
     49        return cachedBBox;
     50    }
     51
     52    protected void addRelationMember(VectorRelationMember member) {
     53        this.members.add(member);
     54        member.getMember().addReferrer(this);
     55        cachedBBox = null;
     56    }
     57
     58    /**
     59     * Remove the first instance of a member from the relation
     60     *
     61     * @param member The member to remove
     62     */
     63    protected void removeRelationMember(VectorRelationMember member) {
     64        this.members.remove(member);
     65        if (!this.members.contains(member)) {
     66            member.getMember().removeReferrer(this);
     67        }
     68    }
     69
     70    @Override public int getMembersCount() {
     71        return this.members.size();
     72    }
     73
     74    @Override public VectorRelationMember getMember(int index) {
     75        return this.members.get(index);
     76    }
     77
     78    @Override public List<VectorRelationMember> getMembers() {
     79        return Collections.unmodifiableList(this.members);
     80    }
     81
     82    @Override public void setMembers(List<VectorRelationMember> members) {
     83        this.members.clear();
     84        this.members.addAll(members);
     85    }
     86
     87    @Override public long getMemberId(int idx) {
     88        return this.getMember(idx).getMember().getId();
     89    }
     90
     91    @Override public String getRole(int idx) {
     92        return this.getMember(idx).getRole();
     93    }
     94
     95    @Override public OsmPrimitiveType getMemberType(int idx) {
     96        return this.getMember(idx).getType();
     97    }
     98
     99    @Override public OsmPrimitiveType getType() {
     100        return this.getMembers().stream().map(VectorRelationMember::getType)
     101          .allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
     102    }
     103}
  • new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
    new file mode 100644
    - +  
     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 public String getRole() {
     32        return this.role;
     33    }
     34
     35    @Override public boolean isNode() {
     36        return this.member instanceof INode;
     37    }
     38
     39    @Override public boolean isWay() {
     40        return this.member instanceof IWay;
     41    }
     42
     43    @Override public boolean isRelation() {
     44        return this.member instanceof IRelation;
     45    }
     46
     47    @Override public VectorPrimitive getMember() {
     48        return this.member;
     49    }
     50
     51    @Override public long getUniqueId() {
     52        return this.member.getId();
     53    }
     54
     55    @Override public OsmPrimitiveType getType() {
     56        return this.member.getType();
     57    }
     58
     59    @Override public boolean isNew() {
     60        return this.member.isNew();
     61    }
     62}
  • new file src/org/openstreetmap/josm/data/vector/VectorWay.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java
    new file mode 100644
    - +  
     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 public UniqueIdGenerator getIdGenerator() {
     36        return WAY_GENERATOR;
     37    }
     38
     39    @Override public void accept(PrimitiveVisitor visitor) {
     40        visitor.visit(this);
     41    }
     42
     43    @Override public BBox getBBox() {
     44        if (cachedBBox == null) {
     45            cachedBBox = new BBox();
     46            for (INode node : this.getNodes()) {
     47                cachedBBox.add(node.getBBox());
     48            }
     49        }
     50        return cachedBBox;
     51    }
     52
     53    @Override public int getNodesCount() {
     54        return this.getNodes().size();
     55    }
     56
     57    @Override public VectorNode getNode(int index) {
     58        return this.getNodes().get(index);
     59    }
     60
     61    @Override public List<VectorNode> getNodes() {
     62        return Collections.unmodifiableList(this.nodes);
     63    }
     64
     65    @Override public void setNodes(List<VectorNode> nodes) {
     66        this.nodes.forEach(node -> node.removeReferrer(this));
     67        this.nodes.clear();
     68        nodes.forEach(node -> node.addReferrer(this));
     69        this.nodes.addAll(nodes);
     70        this.cachedBBox = null;
     71    }
     72
     73    @Override public List<Long> getNodeIds() {
     74        return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList());
     75    }
     76
     77    @Override public long getNodeId(int idx) {
     78        return this.getNodes().get(idx).getId();
     79    }
     80
     81    @Override public boolean isClosed() {
     82        return this.firstNode() != null && this.firstNode().equals(this.lastNode());
     83    }
     84
     85    @Override public VectorNode firstNode() {
     86        if (this.nodes.isEmpty()) {
     87            return null;
     88        }
     89        return this.getNode(0);
     90    }
     91
     92    @Override public VectorNode lastNode() {
     93        if (this.nodes.isEmpty()) {
     94            return null;
     95        }
     96        return this.getNode(this.getNodesCount() - 1);
     97    }
     98
     99    @Override public boolean isFirstLastNode(INode n) {
     100        if (this.nodes.isEmpty()) {
     101            return false;
     102        }
     103        return this.firstNode().equals(n) || this.lastNode().equals(n);
     104    }
     105
     106    @Override public boolean isInnerNode(INode n) {
     107        if (this.nodes.isEmpty()) {
     108            return false;
     109        }
     110        return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream()
     111          .anyMatch(vectorNode -> vectorNode.equals(n));
     112    }
     113
     114    @Override public OsmPrimitiveType getType() {
     115        return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
     116    }
     117}
  • src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
    a b  
    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.
     
    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) {
     
    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
     
    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();
     
    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();
     
    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)) {
     
    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.
     
    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
     
    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;
     
    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));
     
    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;
     
    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 {
     
    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;
     
    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            }
     
    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/RelationSorter.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
    a b  
    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;
     
    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/dialogs/relation/sort/RelationSortUtils.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
    a b  
    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/**
     
    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 */
     
    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}
  • new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
    a b  
    8787import org.openstreetmap.josm.data.imagery.OffsetBookmark;
    8888import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    8989import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
     90import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
    9091import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    9192import org.openstreetmap.josm.data.preferences.BooleanProperty;
    9293import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    110111import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction;
    111112import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction;
    112113import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction;
     114import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
    113115import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
    114116import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction;
    115117import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
     
    890892            if (coordinateConverter.requiresReprojection()) {
    891893                tile = new ReprojectionTile(tileSource, x, y, zoom);
    892894            } else {
    893                 tile = new Tile(tileSource, x, y, zoom);
     895                tile = createTile(tileSource, x, y, zoom);
    894896            }
    895897            tileCache.addTile(tile);
    896898        }
     
    10431045                    img = getLoadedTileImage(tile);
    10441046                    anchorImage = getAnchor(tile, img);
    10451047                }
    1046                 if (img == null || anchorImage == null) {
     1048                if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
    10471049                    miss = true;
    10481050                }
    10491051            }
     
    10521054                return;
    10531055            }
    10541056
    1055             img = applyImageProcessors(img);
     1057            if (img != null) {
     1058                img = applyImageProcessors(img);
     1059            }
    10561060
    10571061            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    10581062            synchronized (paintMutex) {
     
    18641868
    18651869                for (int x = minX; x <= maxX; x++) {
    18661870                    for (int y = minY; y <= maxY; y++) {
    1867                         requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
     1871                        requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
    18681872                    }
    18691873                }
    18701874            }
     
    19701974        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    19711975    }
    19721976
     1977    /**
     1978     * Create a new tile. Added to allow use of custom {@link Tile} objects.
     1979     *
     1980     * @param source Tile source
     1981     * @param x X coordinate
     1982     * @param y Y coordinate
     1983     * @param zoom Zoom level
     1984     * @return The new {@link Tile}
     1985     * @since xxx
     1986     */
     1987    public Tile createTile(T source, int x, int y, int zoom) {
     1988        return new Tile(source, x, y, zoom);
     1989    }
     1990
    19731991    @Override
    19741992    public synchronized void destroy() {
    19751993        super.destroy();
     
    19902008            allocateCacheMemory();
    19912009            if (memory != null) {
    19922010                doPaint(graphics);
     2011                if (AbstractTileSourceLayer.this instanceof MVTLayer) {
     2012                    AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView()
     2013                      .getRealBounds());
     2014                }
    19932015            } else {
    19942016                Graphics g = graphics.getDefaultGraphics();
    19952017                Color oldColor = g.getColor();
  • src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java b/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
    a b  
    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     */
     
    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                 }
    169             } else {
    170                 boolean hasNonModifier = false;
    171                 boolean hasText = false;
    172                 for (StyleElement s : p.a) {
    173                     if (s instanceof BoxTextElement) {
    174                         hasText = true;
    175                     } else {
    176                         if (!s.isModifier) {
    177                             hasNonModifier = true;
    178                         }
    179                     }
    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);
    185                     }
    186                 }
    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);
    201                         break;
    202                     }
    203                 }
    204                 p.a = new StyleElementList(p.a, line);
    205             }
    206         }
    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;
     165        synchronized (osm.getStyleCacheSyncObject()) {
     166            if (!osm.isCachedStyleUpToDate() || scale <= 0) {
     167                osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE);
     168            } else {
     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;
     178                    } else {
     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                            }
     191                        }
     192                    }
     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                        }
     198                    }
     199                }
     200            } else if (osm instanceof IWay && isDefaultLines()) {
     201                boolean hasProperLineStyle = false;
     202                for (StyleElement s : p.a) {
     203                    if (s.isProperLineStyle()) {
     204                        hasProperLineStyle = true;
     205                        break;
     206                    }
     207                }
     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                }
     218            }
     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;
     229        }
    217230    }
    218231
    219232    /**
     
    241254     * @param nc navigable component
    242255     * @return pair containing style list and range
    243256     */
    244     private Pair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) {
     257    Pair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) {
    245258        if (osm instanceof INode)
    246259            return generateStyles(osm, scale, false);
    247260        else if (osm instanceof IWay) {
     
    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);
  • new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.preferences.imagery;
     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

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
    new file mode 100644
    - +  
     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.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.assertTrue;
     7
     8
     9import java.nio.file.Paths;
     10import java.text.MessageFormat;
     11import java.util.Collection;
     12import java.util.Map;
     13import java.util.Objects;
     14import java.util.Optional;
     15import java.util.stream.Collectors;
     16
     17import org.openstreetmap.josm.TestUtils;
     18import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     19import org.openstreetmap.josm.gui.mappaint.Keyword;
     20import org.openstreetmap.josm.gui.mappaint.StyleSource;
     21import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
     22import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
     23import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
     24import org.openstreetmap.josm.testutils.JOSMTestRules;
     25import org.openstreetmap.josm.tools.ColorHelper;
     26
     27import org.junit.jupiter.api.Test;
     28import org.junit.jupiter.api.extension.RegisterExtension;
     29
     30/**
     31 * Test class for {@link MapBoxVectorStyle}
     32 * @author Taylor Smock
     33 */
     34public class MapBoxVectorStyleTest {
     35    // Needed for osm primitives (we really just need to initialize the config)
     36    // OSM primitives are called when we load style sources
     37    @RegisterExtension
     38    JOSMTestRules rules = new JOSMTestRules();
     39
     40    @Test
     41    void testMapillaryStyle() {
     42        final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString();
     43        final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file);
     44        assertNotNull(style);
     45        // There are three "sources" in the mapillary.json file
     46        assertEquals(3, style.getSources().size());
     47        final ElemStyles mapillarySource = style.getSources().entrySet().stream()
     48          .filter(source -> "mapillary-source".equals(source.getKey().getName())).map(
     49            Map.Entry::getValue).findAny().orElse(null);
     50        assertNotNull(mapillarySource);
     51        mapillarySource.getStyleSources().forEach(StyleSource::loadStyleSource);
     52        assertEquals(1, mapillarySource.getStyleSources().size());
     53        final MapCSSStyleSource mapillaryCssSource = (MapCSSStyleSource) mapillarySource.getStyleSources().get(0);
     54        assertTrue(mapillaryCssSource.getErrors().isEmpty());
     55        final MapCSSRule mapillaryOverview = getRule(mapillaryCssSource, "node", "mapillary-overview");
     56        assertNotNull(mapillaryOverview);
     57        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-shape", new Keyword("circle"));
     58        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-color", ColorHelper.html2color("#05CB63"));
     59        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-opacity", 0.6f);
     60        assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-size", 4.0f);
     61    }
     62
     63    /**
     64     * Check that an instruction is in a collection of instructions, and return it
     65     * @param instructions The instructions to search
     66     * @param key The key to look for
     67     * @param value The expected value for the key
     68     */
     69    private void assertInInstructions(Collection<Instruction> instructions, String key, Object value) {
     70        // In JOSM, all Instruction objects are AssignmentInstruction objects
     71        Collection<Instruction.AssignmentInstruction> instructionKeys = instructions.stream().filter(Instruction.AssignmentInstruction.class::isInstance).map(
     72          Instruction.AssignmentInstruction.class::cast).filter(instruction -> Objects.equals(key, instruction.key)).collect(
     73          Collectors.toList());
     74        Optional<Instruction.AssignmentInstruction> instructionOptional = instructionKeys.stream().filter(instruction -> Objects.equals(value, instruction.val)).findAny();
     75        assertTrue(instructionOptional.isPresent(), MessageFormat
     76          .format("Expected {0}, but got {1}", value, instructionOptional.orElse(instructionKeys.stream().findAny().orElseThrow(() -> new AssertionError("No instruction with "+key+" found"))).val));
     77    }
     78
     79    private static MapCSSRule getRule(MapCSSStyleSource source, String base, String subpart) {
     80        return source.rules.stream().filter(rule -> rule.selectors.stream().filter(selector -> base.equals(selector.getBase()) && subpart.equals(selector.getSubpart().getId(null))).count() > 0).findAny().orElse(null);
     81    }
     82}
  • new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
    new file mode 100644
    - +  
     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 */
     35 class 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 JOSMTestRules rule = new JOSMTestRules().projection();
     73
     74    /**
     75     * Load arbitrary tiles
     76     * @param layer The layer to add the tiles to
     77     * @param tiles The tiles to load ([z, x, y, z, x, y, ...]) -- must be divisible by three
     78     */
     79    private static void loadTile(MVTLayerMock layer, int... tiles) {
     80        if (tiles.length % 3 != 0 || tiles.length == 0) {
     81            throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component");
     82        }
     83        final MapboxVectorTileSource tileSource = layer.getTileSource();
     84        MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader();
     85        Collection<MVTTile> tilesCollection = new ArrayList<>();
     86        for (int i = 0; i < tiles.length / 3; i++) {
     87            final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]);
     88            tileLoader.createTileLoaderJob(tile).submit();
     89            tilesCollection.add(tile);
     90        }
     91        Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> layer.finishedLoading().size() == tilesCollection
     92          .size());
     93    }
     94
     95    private MVTLayerMock layer;
     96
     97    @BeforeEach
     98    void setup() {
     99        // Create the preconditions for the test
     100        final ImageryInfo info = new ImageryInfo();
     101        info.setName("en", "Test info");
     102        info.setUrl("file:/" + Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "{z}", "{x}", "{y}.mvt"));
     103        layer = new MVTLayerMock(info);
     104    }
     105
     106    @Test
     107    void testNodeDeduplication() {
     108        final VectorDataSet dataSet = this.layer.getData();
     109        assertTrue(dataSet.allPrimitives().isEmpty());
     110
     111        // Set the zoom to 14, as that is the tile we are checking
     112        dataSet.setZoom(14);
     113        loadTile(this.layer, 14, 3248, 6258);
     114
     115        // Actual test
     116        // With Mapillary, only ends of ways should be untagged
     117        // There are 55 actual "nodes" in the data with two nodes for the ends of the way.
     118        assertEquals(57, dataSet.getNodes().size());
     119        assertEquals(1, dataSet.getWays().size());
     120        assertEquals(0, dataSet.getRelations().size());
     121    }
     122
     123    @Test
     124    void testWayDeduplicationSimple() {
     125        final VectorDataSet dataSet = this.layer.getData();
     126        assertTrue(dataSet.allPrimitives().isEmpty());
     127
     128        // Set the zoom to 14, as that is the tile we are checking
     129        dataSet.setZoom(14);
     130        // Load tiles that are next to each other
     131        loadTile(this.layer, 14, 3248, 6258, 14, 3248, 6257);
     132
     133        Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream()
     134          .collect(Collectors.groupingBy(VectorWay::getId));
     135        wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id)));
     136    }
     137}