001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.imagery; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.Graphics2D; 008import java.awt.event.ActionEvent; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Map; 016import java.util.Objects; 017import java.util.function.BooleanSupplier; 018import java.util.function.Consumer; 019import java.util.stream.Collectors; 020 021import javax.swing.AbstractAction; 022import javax.swing.Action; 023import javax.swing.JCheckBoxMenuItem; 024import javax.swing.JMenuItem; 025 026import org.apache.commons.jcs3.access.CacheAccess; 027import org.openstreetmap.gui.jmapviewer.Tile; 028import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 029import org.openstreetmap.josm.actions.ExpertToggleAction; 030import org.openstreetmap.josm.data.Bounds; 031import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 032import org.openstreetmap.josm.data.imagery.ImageryInfo; 033import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 034import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile; 035import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; 036import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener; 037import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorCachedTileLoader; 038import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource; 039import org.openstreetmap.josm.data.osm.DataSet; 040import org.openstreetmap.josm.data.osm.Node; 041import org.openstreetmap.josm.data.osm.OsmPrimitive; 042import org.openstreetmap.josm.data.osm.Relation; 043import org.openstreetmap.josm.data.osm.RelationMember; 044import org.openstreetmap.josm.data.osm.Way; 045import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 046import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 047import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 048import org.openstreetmap.josm.data.vector.VectorDataSet; 049import org.openstreetmap.josm.data.vector.VectorNode; 050import org.openstreetmap.josm.data.vector.VectorPrimitive; 051import org.openstreetmap.josm.data.vector.VectorRelation; 052import org.openstreetmap.josm.data.vector.VectorWay; 053import org.openstreetmap.josm.gui.MainApplication; 054import org.openstreetmap.josm.gui.MapView; 055import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 056import org.openstreetmap.josm.gui.layer.LayerManager; 057import org.openstreetmap.josm.gui.layer.OsmDataLayer; 058import org.openstreetmap.josm.gui.mappaint.ElemStyles; 059import org.openstreetmap.josm.gui.mappaint.StyleSource; 060 061/** 062 * A layer for Mapbox Vector Tiles 063 * @author Taylor Smock 064 * @since 17862 065 */ 066public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements TileListener { 067 private static final String CACHE_REGION_NAME = "MVT"; 068 // Just to avoid allocating a bunch of 0 length action arrays 069 private static final Action[] EMPTY_ACTIONS = new Action[0]; 070 private final Map<String, Boolean> layerNames = new HashMap<>(); 071 private final VectorDataSet dataSet = new VectorDataSet(); 072 073 /** 074 * Creates an instance of an MVT layer 075 * 076 * @param info ImageryInfo describing the layer 077 */ 078 public MVTLayer(ImageryInfo info) { 079 super(info); 080 } 081 082 @Override 083 protected Class<? extends TileLoader> getTileLoaderClass() { 084 return MapboxVectorCachedTileLoader.class; 085 } 086 087 @Override 088 protected String getCacheName() { 089 return CACHE_REGION_NAME; 090 } 091 092 /** 093 * Returns cache region for MVT layer. 094 * @return cache region for MVT layer 095 * @since 18186 096 */ 097 public static CacheAccess<String, BufferedImageCacheEntry> getCache() { 098 return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME); 099 } 100 101 @Override 102 public Collection<String> getNativeProjections() { 103 // Mapbox Vector Tiles <i>specifically</i> only support EPSG:3857 104 // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}"). 105 return Collections.singleton(MVTFile.DEFAULT_PROJECTION); 106 } 107 108 @Override 109 public void paint(Graphics2D g, MapView mv, Bounds box) { 110 this.dataSet.setZoom(this.getZoomLevel()); 111 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, false); 112 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 113 || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 114 // Set the painter to use our custom style sheet 115 if (painter instanceof StyledMapRenderer && this.dataSet.getStyles() != null) { 116 ((StyledMapRenderer) painter).setStyles(this.dataSet.getStyles()); 117 } 118 painter.render(this.dataSet, false, box); 119 } 120 121 @Override 122 protected MapboxVectorTileSource getTileSource() { 123 MapboxVectorTileSource source = new MapboxVectorTileSource(this.info); 124 this.info.setAttribution(source); 125 if (source.getStyleSource() != null) { 126 List<ElemStyles> styles = source.getStyleSource().getSources().entrySet().stream() 127 .filter(entry -> entry.getKey() == null || entry.getKey().getUrls().contains(source.getBaseUrl())) 128 .map(Map.Entry::getValue).collect(Collectors.toList()); 129 // load the style sources 130 styles.stream().map(ElemStyles::getStyleSources).flatMap(Collection::stream).forEach(StyleSource::loadStyleSource); 131 this.dataSet.setStyles(styles); 132 this.setName(source.getName()); 133 } 134 return source; 135 } 136 137 @Override 138 public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) { 139 final MVTTile tile = new MVTTile(source, x, y, zoom); 140 tile.addTileLoaderFinisher(this); 141 return tile; 142 } 143 144 @Override 145 public Action[] getMenuEntries() { 146 ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries())); 147 // Add separator between Info and the layers 148 actions.add(SeparatorLayerAction.INSTANCE); 149 if (ExpertToggleAction.isExpert()) { 150 for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) { 151 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true), 152 layer -> { 153 layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value)); 154 this.dataSet.setInvisibleLayers(layerNames.entrySet().stream() 155 .filter(entry -> Boolean.FALSE.equals(entry.getValue())) 156 .map(Map.Entry::getKey).collect(Collectors.toList())); 157 this.invalidate(); 158 })); 159 } 160 // Add separator between layers and convert action 161 actions.add(SeparatorLayerAction.INSTANCE); 162 actions.add(new ConvertLayerAction(this)); 163 } 164 return actions.toArray(EMPTY_ACTIONS); 165 } 166 167 /** 168 * Get the data set for this layer 169 */ 170 public VectorDataSet getData() { 171 return this.dataSet; 172 } 173 174 private static class ConvertLayerAction extends AbstractAction implements LayerAction { 175 private final MVTLayer layer; 176 177 ConvertLayerAction(MVTLayer layer) { 178 this.layer = layer; 179 } 180 181 @Override 182 public void actionPerformed(ActionEvent e) { 183 LayerManager manager = MainApplication.getLayerManager(); 184 VectorDataSet dataSet = layer.getData(); 185 DataSet osmData = new DataSet(); 186 // Add nodes first, map is to ensure we can map new nodes to vector nodes 187 Map<VectorNode, Node> nodeMap = new HashMap<>(dataSet.getNodes().size()); 188 for (VectorNode vectorNode : dataSet.getNodes()) { 189 Node newNode = new Node(vectorNode.getCoor()); 190 if (vectorNode.isTagged()) { 191 vectorNode.getInterestingTags().forEach(newNode::put); 192 newNode.put("layer", vectorNode.getLayer()); 193 newNode.put("id", Long.toString(vectorNode.getId())); 194 } 195 nodeMap.put(vectorNode, newNode); 196 } 197 // Add ways next 198 Map<VectorWay, Way> wayMap = new HashMap<>(dataSet.getWays().size()); 199 for (VectorWay vectorWay : dataSet.getWays()) { 200 Way newWay = new Way(); 201 List<Node> nodes = vectorWay.getNodes().stream().map(nodeMap::get).filter(Objects::nonNull).collect(Collectors.toList()); 202 newWay.setNodes(nodes); 203 if (vectorWay.isTagged()) { 204 vectorWay.getInterestingTags().forEach(newWay::put); 205 newWay.put("layer", vectorWay.getLayer()); 206 newWay.put("id", Long.toString(vectorWay.getId())); 207 } 208 wayMap.put(vectorWay, newWay); 209 } 210 211 // Finally, add Relations 212 Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size()); 213 for (VectorRelation vectorRelation : dataSet.getRelations()) { 214 Relation newRelation = new Relation(); 215 if (vectorRelation.isTagged()) { 216 vectorRelation.getInterestingTags().forEach(newRelation::put); 217 newRelation.put("layer", vectorRelation.getLayer()); 218 newRelation.put("id", Long.toString(vectorRelation.getId())); 219 } 220 List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> { 221 final OsmPrimitive primitive; 222 final VectorPrimitive vectorPrimitive = member.getMember(); 223 if (vectorPrimitive instanceof VectorNode) { 224 primitive = nodeMap.get(vectorPrimitive); 225 } else if (vectorPrimitive instanceof VectorWay) { 226 primitive = wayMap.get(vectorPrimitive); 227 } else if (vectorPrimitive instanceof VectorRelation) { 228 // Hopefully, relations are encountered in order... 229 primitive = relationMap.get(vectorPrimitive); 230 } else { 231 primitive = null; 232 } 233 if (primitive == null) return null; 234 return new RelationMember(member.getRole(), primitive); 235 }).filter(Objects::nonNull).collect(Collectors.toList()); 236 newRelation.setMembers(members); 237 relationMap.put(vectorRelation, newRelation); 238 } 239 try { 240 osmData.beginUpdate(); 241 nodeMap.values().forEach(osmData::addPrimitive); 242 wayMap.values().forEach(osmData::addPrimitive); 243 relationMap.values().forEach(osmData::addPrimitive); 244 } finally { 245 osmData.endUpdate(); 246 } 247 manager.addLayer(new OsmDataLayer(osmData, this.layer.getName(), null)); 248 manager.removeLayer(this.layer); 249 } 250 251 @Override 252 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 253 return layers.stream().allMatch(MVTLayer.class::isInstance); 254 } 255 256 @Override 257 public Component createMenuComponent() { 258 JMenuItem menuItem = new JMenuItem(tr("Convert to OSM Data")); 259 menuItem.addActionListener(this); 260 return menuItem; 261 } 262 } 263 264 private static class EnableLayerAction extends AbstractAction implements LayerAction { 265 private final String layer; 266 private final Consumer<String> consumer; 267 private final BooleanSupplier state; 268 269 EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) { 270 super(tr("Toggle layer {0}", layer)); 271 this.layer = layer; 272 this.consumer = consumer; 273 this.state = state; 274 } 275 276 @Override 277 public void actionPerformed(ActionEvent e) { 278 consumer.accept(layer); 279 } 280 281 @Override 282 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 283 return layers.stream().allMatch(MVTLayer.class::isInstance); 284 } 285 286 @Override 287 public Component createMenuComponent() { 288 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 289 item.setSelected(this.state.getAsBoolean()); 290 return item; 291 } 292 } 293 294 @Override 295 public void finishedLoading(MVTTile tile) { 296 for (Layer layer : tile.getLayers()) { 297 this.layerNames.putIfAbsent(layer.getName(), true); 298 } 299 this.dataSet.addTileData(tile); 300 } 301}