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}