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}