/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2017, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * */ package org.geotools.mbstyle.layer; import org.geotools.mbstyle.MBStyle; import org.geotools.mbstyle.parse.MBFilter; import org.geotools.mbstyle.parse.MBFormatException; import org.geotools.mbstyle.parse.MBObjectParser; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Rule; import org.geotools.styling.StyleFactory2; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.style.SemanticType; import java.util.Collections; import java.util.List; /** * MBLayer wrapper (around one of the MBStyle layers). * <p> * All methods act as accessors on provided JSON layer, no other state is maintained. This allows * modifications to be made cleanly with out chance of side-effect. * * <ul> * <li>get methods: access the json directly</li> * <li>query methods: provide logic / transforms to GeoTools classes as required.</li> * </ul> * * <p> * In the normal course of events MBLayer is constructed as a flyweight object by MBStyle to * provide easy access to its layers list. * </p> * * @author Torben Barsballe (Boundless) */ public abstract class MBLayer { /** Helper class used to provide json access and expression / filter conversions */ final protected MBObjectParser parse; /** Shared filter factory */ final protected FilterFactory2 ff; /** Shared style factory */ final protected StyleFactory2 sf; /** JSON layer being wrapped. */ final protected JSONObject json; /** field access for "id" key (due to its use in error messages) */ final protected String id; /** * Whether this layer is displayed. * * Optional enum. One of visible, none. Defaults to visible. */ public static enum Visibility { /** The layer is shown. */ VISIBLE, /** The layer is not shown. */ NONE } public MBLayer(JSONObject json, MBObjectParser parse) { this.json = json; this.parse = parse; this.ff = parse.getFilterFactory(); this.sf = parse.getStyleFactory(); this.id = (String) json.get("id"); } // // Factory methods // /** * Factory method creating the appropriate MBStyle based on the "type" indicated in the layer JSON. */ public static MBLayer create(JSONObject layer) { if (layer.containsKey("type") && layer.get("type") instanceof String) { String type = (String) layer.get("type"); switch (type) { case "line": return new LineMBLayer(layer); case "fill": return new FillMBLayer(layer); case "raster": return new RasterMBLayer(layer); case "circle": return new CircleMBLayer(layer); case "background": return new BackgroundMBLayer(layer); case "symbol": return new SymbolMBLayer(layer); case "fill-extrusion": return new FillExtrusionMBLayer(layer); default: throw new MBFormatException(("\"type\" " + type + " is not a valid layer type. Must be one of: " + "background, fill, line, symbol, raster, circle, fill-extrusion")); } } // technically we may be able to do this via a ref throw new MBFormatException("\"type\" required to create layer."); } /** * Rendering type of this layer. * <p> * One of: * <ul> * <li>fill:A filled polygon with an optional stroked border.</li> * <li>line: A stroked line.</li> * <li>symbol: An icon or a text label.</li> * <li>circle:A filled circle.</li> * <li>fill-extrusion: An extruded (3D) polygon.</li> * <li>raster: Raster map textures such as satellite imagery.</li> * <li>background: The background color or pattern of the map.</li> * </ul> * * @return One of fill, line, symbol, circle, fill-extrusion, raster, background. */ public abstract String getType(); /** * Arbitrary properties useful to track with the layer, but do not influence rendering. * Properties should be prefixed to avoid collisions, like 'mapbox:' and 'gt:`. * * @return Arbitrary properties useful to track with the layer. */ public JSONObject getMetadata() { return parse.getJSONObject(json, "metadata", new JSONObject()); } /** * References another layer to copy type, source, source-layer, minzoom, maxzoom, filter, and * layout properties from. This allows the layers to share processing and be more efficient. * * @return References another layer to copy type, source, source-layer, minzoom, maxzoom, filter, and * layout properties from. */ public String getRef(){ // We should update getType(), getSource(), getSourceLayer(), getMinZoom(), getMaxZoom(), // getFilter() to look up value provided by getRef() if needed. return parse.optional(String.class, json, "ref", null); } public JSONObject getJson() { return json; } /** * Unique layer name. * @return layer name, required field */ public String getId() { return id; } /** * Name of a source description to be used for this layer. * <p> * While this value is optional, it may be obtained via {@link #getRef()} if needed. * </p> * * @return name of source description to be used for this layer, or null if the style has no source. */ public String getSource() { return parse.optional(String.class, json, "source", null); } /** * Layer to use from a vector tile source. Required if the source supports multiple layers. * <p> * While this value is optional, it may be obtained via {@link #getRef()} if needed. * </p> * * @return layer to use from a vector tile source, or null if the style has no source-layer. */ public String getSourceLayer() { return parse.optional(String.class, json, "source-layer", null); } /** * The minimum zoom level on which the layer gets parsed and appears on. * * @return minimum zoom level, or Integer.MIN_VALUE if the style has no minzoom. */ public int getMinZoom() { Integer min = parse.optional(Integer.class, json, "minzoom", null); return min == null ? Integer.MIN_VALUE : min; } /** * The maximum zoom level on which the layer gets parsed and appears on. * * @return maximum zoom level, or Integer.MAX_VALUE if the style has no maxzoom. */ public int getMaxZoom() { Integer max = parse.optional(Integer.class, json, "maxzoom", null); return max == null ? Integer.MAX_VALUE : max; } /** * A MBFilter wrapping optional json specifying conditions on source features. Only features * that match the filter are displayed. This is available as a GeoTools {@link Filter} via * {@link #filter()}. * * @return MBFilter expression specifying conditions on source features. */ public MBFilter getFilter(){ JSONArray array = parse.getJSONArray(json,"filter", null ); MBFilter filter = new MBFilter(array, parse, defaultSemanticType()); return filter; } /** * Default {@link SemanticType} to use when generating {@link #getFilter()}. * <p> * Use ANY to match all geometry, or fill in LINE, POINT, POLYGON if needed.</p> * * @return Appropriate LINE, POINT, POLYGON value, or ANY to match any geometry. */ abstract SemanticType defaultSemanticType(); /** * The "filter" as a GeoTools {@link Filter} suitable for feature selection, as defined by * {@link #getFilter()}. * * @return Filter, or Filter.INCLUDE if the style has no filter. */ public Filter filter(){ MBFilter mbFilter = getFilter(); if (mbFilter == null) { return Filter.INCLUDE; } return mbFilter.filter(); } /** * Layout properties for the layer. * <p> * <em>Layout properties</em> appear in the layer's "layout" object. They are applied early in * the rendering process and define how data for that layer is passed to the renderer. For * efficiency, a layer can share layout properties with another layer via the "ref" layer * property, and should do so where possible. This will decrease processing time and allow the * two layers will share GPU memory and other resources associated with the layer. * </p> * * @return Layout properties defined for layer, or an empty {@link JSONObject} if no layout properties are defined * for the style. */ public JSONObject getLayout(){ return parse.layout(json); } /** * Query for layout information (making use of {@link #getRef()} if available). * * @return Layout properties to use for this layer. */ public JSONObject layout(){ return getLayout(); } /** * Default paint properties for this layer. * <p> * <em>Paint properties</em> are applied later in the rendering process. A layer that shares layout * properties with another layer can have independent paint properties. Paint properties appear * in the layer's "paint" object. * </p> * * @return Default paint properties for this layer, or an empty {@link JSONObject} if no paint properties are * defined for the style. */ public JSONObject getPaint(){ return parse.paint(json); } /** * Layout setting - whether this layer is displayed. * * @return One of visible, none. Defaults to visible if not defined in the style. */ public Visibility getVisibility(){ JSONObject layout = layout(); return parse.getEnum( layout, "visibility", Visibility.class, Visibility.VISIBLE ); } /** * Whether this layer is displayed. * * @return Whether the layout is visible. Defaults to true. */ public boolean visibility(){ return getVisibility() == Visibility.VISIBLE; } /** * Query for paint information (making use of {@link #getRef()} if available). * * @return Paint properties to use for this layer. */ public JSONObject paint(){ return getPaint(); } /** * Class-specific "paint.*" properties for this layer. The class name is the part after the first * dot. * * @return class specific "paint.*" properties * @deprecated style classes are deprecated and will be removed in the next version of this spec. */ public JSONObject getPaintProperties(){ return new JSONObject(); } /** * Transforms a given {@link MBLayer} to a GeoTools {@link FeatureTypeStyle}. * * @param minScaleDenominator Used to determine zoom level restrictions for generated rules * @param maxScaleDenominator Used to determine zoom level restrictions for generated rules * @return A feature type style from the provided layer, or null if the visibility of that layer is false. */ public List<FeatureTypeStyle> transform(MBStyle styleContext, Double minScaleDenominator, Double maxScaleDenominator) { // Would prefer to accept zoom levels here (less concepts in our API) // If we accept zoom levels we may be able to reduce, and return a list of FeatureTypeStyles // (with the understanding that the list may be empty if the MBLayer does not contribute any content // at a specific zoom level range) List<FeatureTypeStyle> style = transform(styleContext); if (style == null) { return Collections.emptyList(); } for (FeatureTypeStyle fts : style) { for (Rule rule : fts.rules()) { if (minScaleDenominator != null) { rule.setMinScaleDenominator(minScaleDenominator); } if (maxScaleDenominator != null) { rule.setMaxScaleDenominator(maxScaleDenominator); } } } return style; } /** * * Transforms a given {@link MBLayer} to a GeoTools {@link FeatureTypeStyle}. * * @param styleContext The MBStyle to which this layer belongs, used as a context for things like resolving sprite and glyph names to full urls. * @return A feature type style from the provided layer, or null if the visibility of that layer is false. */ public final List<FeatureTypeStyle> transform(MBStyle styleContext) { MBLayer layer = this; if (!layer.visibility()) { return null; // layer layout visibility 'none' } return transformInternal(styleContext); } public abstract List<FeatureTypeStyle> transformInternal(MBStyle styleContext); // // Data Object based on wrapped json // /** Hashcode based on wrapped {@link #json}. */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((json == null) ? 0 : json.hashCode()); return result; } /** Equality based on wrapped {@link #json}. */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; MBLayer other = (MBLayer) obj; if (json == null) { if (other.json != null) return false; } else if (!json.equals(other.json)) return false; return true; } @Override public String toString() { return getClass().getSimpleName() + " id=" + id; } }