001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
003
004import java.awt.image.BufferedImage;
005import java.io.IOException;
006import java.io.InputStream;
007import java.util.Collection;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Objects;
011import java.util.stream.Collectors;
012
013import org.openstreetmap.gui.jmapviewer.Tile;
014import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
015import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
016import org.openstreetmap.josm.data.IQuadBucketType;
017import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
018import org.openstreetmap.josm.data.osm.BBox;
019import org.openstreetmap.josm.data.protobuf.ProtobufParser;
020import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
021import org.openstreetmap.josm.data.vector.VectorDataStore;
022import org.openstreetmap.josm.tools.ListenerList;
023import org.openstreetmap.josm.tools.Logging;
024
025/**
026 * A class for Mapbox Vector Tiles
027 *
028 * @author Taylor Smock
029 * @since 17862
030 */
031public class MVTTile extends Tile implements VectorTile, IQuadBucketType {
032    private final ListenerList<TileListener> listenerList = ListenerList.create();
033    private Collection<Layer> layers;
034    private int extent = Layer.DEFAULT_EXTENT;
035    static final BufferedImage CLEAR_LOADED = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
036    private BBox bbox;
037    private VectorDataStore vectorDataStore;
038
039    /**
040     * Create a new Tile
041     * @param source The source of the tile
042     * @param xtile The x coordinate for the tile
043     * @param ytile The y coordinate for the tile
044     * @param zoom The zoom for the tile
045     */
046    public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
047        super(source, xtile, ytile, zoom);
048    }
049
050    @Override
051    public void loadImage(final InputStream inputStream) throws IOException {
052        if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
053            this.initLoading();
054            ProtobufParser parser = new ProtobufParser(inputStream);
055            Collection<ProtobufRecord> protobufRecords = parser.allRecords();
056            this.layers = new HashSet<>();
057            this.layers = protobufRecords.stream().map(protoBufRecord -> {
058                Layer mvtLayer = null;
059                if (protoBufRecord.getField() == Layer.LAYER_FIELD) {
060                    try (ProtobufParser tParser = new ProtobufParser(protoBufRecord.getBytes())) {
061                        mvtLayer = new Layer(tParser.allRecords());
062                    } catch (IOException e) {
063                        Logging.error(e);
064                    } finally {
065                        // Cleanup bytes
066                        protoBufRecord.close();
067                    }
068                }
069                return mvtLayer;
070            }).collect(Collectors.toCollection(HashSet::new));
071            this.extent = layers.stream().filter(Objects::nonNull).mapToInt(Layer::getExtent).max().orElse(Layer.DEFAULT_EXTENT);
072            if (this.getData() != null) {
073                this.finishLoading();
074                this.listenerList.fireEvent(event -> event.finishedLoading(this));
075                // Ensure that we don't keep the loading image around
076                this.image = CLEAR_LOADED;
077                // Cleanup as much as possible -- layers will still exist, but only base information (like name, extent) will remain.
078                // Called last just in case the listeners need the layers.
079                this.layers.forEach(Layer::destroy);
080            }
081        }
082    }
083
084    @Override
085    public Collection<Layer> getLayers() {
086        return this.layers;
087    }
088
089    @Override
090    public int getExtent() {
091        return this.extent;
092    }
093
094    /**
095     * Add a tile loader finisher listener
096     *
097     * @param listener The listener to add
098     */
099    public void addTileLoaderFinisher(TileListener listener) {
100        // Add as weak listeners since we don't want to keep unnecessary references.
101        this.listenerList.addWeakListener(listener);
102    }
103
104    @Override
105    public BBox getBBox() {
106        if (this.bbox == null) {
107            final ICoordinate upperLeft = this.getTileSource().tileXYToLatLon(this);
108            final ICoordinate lowerRight = this.getTileSource()
109                    .tileXYToLatLon(this.getXtile() + 1, this.getYtile() + 1, this.getZoom());
110            BBox newBBox = new BBox(upperLeft.getLon(), upperLeft.getLat(), lowerRight.getLon(), lowerRight.getLat());
111            this.bbox = newBBox.toImmutable();
112        }
113        return this.bbox;
114    }
115
116    /**
117     * Get the datastore for this tile
118     * @return The data
119     */
120    public VectorDataStore getData() {
121        if (this.vectorDataStore == null) {
122            VectorDataStore newDataStore = new VectorDataStore();
123            newDataStore.addDataTile(this);
124            this.vectorDataStore = newDataStore;
125        }
126        return this.vectorDataStore;
127    }
128
129    /**
130     * A class that can be notified that a tile has finished loading
131     *
132     * @author Taylor Smock
133     */
134    public interface TileListener {
135        /**
136         * Called when the MVTTile is finished loading
137         *
138         * @param tile The tile that finished loading
139         */
140        void finishedLoading(MVTTile tile);
141    }
142
143    /**
144     * A class used to set the layers that an MVTTile will show.
145     *
146     * @author Taylor Smock
147     */
148    public interface LayerShower {
149        /**
150         * Get a list of layers to show
151         *
152         * @return A list of layer names
153         */
154        List<String> layersToShow();
155    }
156}