All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.apis.vectortiles.model.StyleBuilder Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.apis.vectortiles.model;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber.ZoomStop;
import org.opentripplanner.framework.collection.ListUtils;
import org.opentripplanner.framework.json.ObjectMappers;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.vertex.Vertex;

/**
 * Builds a Maplibre/Mapbox vector tile
 * layer style.
 */
public class StyleBuilder {

  private static final ObjectMapper OBJECT_MAPPER = ObjectMappers.ignoringExtraFields();
  private static final String TYPE = "type";
  private static final String SOURCE_LAYER = "source-layer";
  private final Map props = new LinkedHashMap<>();
  private final Map paint = new LinkedHashMap<>();
  private final Map layout = new LinkedHashMap<>();
  private final Map line = new LinkedHashMap<>();
  private List filter = List.of();

  public static StyleBuilder ofId(String id) {
    return new StyleBuilder(id);
  }

  public StyleBuilder vectorSourceLayer(VectorSourceLayer source) {
    source(source.vectorSource());
    return sourceLayer(source.vectorLayer());
  }

  public enum LayerType {
    Circle,
    Line,
    Raster,
    Fill,
    Symbol,
  }

  private StyleBuilder(String id) {
    props.put("id", id);
  }

  public StyleBuilder minZoom(int i) {
    props.put("minzoom", i);
    return this;
  }

  public StyleBuilder maxZoom(int i) {
    props.put("maxzoom", i);
    return this;
  }

  /**
   * Which vector tile source this should apply to.
   */
  public StyleBuilder source(TileSource source) {
    props.put("source", source.id());
    return this;
  }

  /**
   * For vector tile sources, specify which source layer in the tile the styles should apply to.
   * There is an unfortunate collision in the name "layer" as it can both refer to a styling layer
   * and the layer inside the vector tile.
   */
  public StyleBuilder sourceLayer(String source) {
    props.put(SOURCE_LAYER, source);
    return this;
  }

  public StyleBuilder typeRaster() {
    return type(LayerType.Raster);
  }

  public StyleBuilder typeCircle() {
    return type(LayerType.Circle);
  }

  public StyleBuilder typeLine() {
    type(LayerType.Line);
    layout.put("line-cap", "round");
    return this;
  }

  public StyleBuilder typeFill() {
    type(LayerType.Fill);
    return this;
  }

  public StyleBuilder typeSymbol() {
    type(LayerType.Symbol);
    return this;
  }

  private StyleBuilder type(LayerType type) {
    props.put(TYPE, type.name().toLowerCase());
    return this;
  }

  public StyleBuilder lineText(String name) {
    layout.put("symbol-placement", "line");
    layout.put("symbol-spacing", 500);
    layout.put("text-field", "{%s}".formatted(name));
    layout.put("text-font", List.of("KlokanTech Noto Sans Regular"));
    layout.put(
      "text-size",
      new ZoomDependentNumber(14, List.of(new ZoomStop(14, 12), new ZoomStop(20, 14))).toJson()
    );
    layout.put("text-max-width", 5);
    layout.put("text-keep-upright", true);
    layout.put("text-rotation-alignment", "map");
    paint.put("text-color", "#000");
    paint.put("text-halo-color", "#fff");
    paint.put("text-halo-blur", 4);
    paint.put("text-halo-width", 3);
    return this;
  }

  public StyleBuilder circleColor(String color) {
    paint.put("circle-color", validateColor(color));
    return this;
  }

  public StyleBuilder circleStroke(String color, int width) {
    paint.put("circle-stroke-color", validateColor(color));
    paint.put("circle-stroke-width", width);
    return this;
  }

  public StyleBuilder circleStroke(String color, ZoomDependentNumber width) {
    paint.put("circle-stroke-color", validateColor(color));
    paint.put("circle-stroke-width", width.toJson());
    return this;
  }

  public StyleBuilder circleRadius(ZoomDependentNumber radius) {
    paint.put("circle-radius", radius.toJson());
    return this;
  }

  // Line styling

  public StyleBuilder lineColor(String color) {
    paint.put("line-color", validateColor(color));
    return this;
  }

  public StyleBuilder lineWidth(float width) {
    paint.put("line-width", width);
    return this;
  }

  public StyleBuilder lineWidth(ZoomDependentNumber zoomStops) {
    paint.put("line-width", zoomStops.toJson());
    return this;
  }

  public StyleBuilder fillColor(String color) {
    paint.put("fill-color", validateColor(color));
    return this;
  }

  public StyleBuilder fillOpacity(float opacity) {
    paint.put("fill-opacity", opacity);
    return this;
  }

  public StyleBuilder fillOutlineColor(String color) {
    paint.put("fill-outline-color", validateColor(color));
    return this;
  }

  /**
   * Hide this layer when the debug client starts. It can be made visible in the UI later.
   */
  public StyleBuilder intiallyHidden() {
    layout.put("visibility", "none");
    return this;
  }

  /**
   * Only apply the style to the given edges.
   */
  @SafeVarargs
  public final StyleBuilder edgeFilter(Class... classToFilter) {
    return filterClasses(classToFilter);
  }

  /**
   * Only apply the style to the given vertices.
   */
  @SafeVarargs
  public final StyleBuilder vertexFilter(Class... classToFilter) {
    return filterClasses(classToFilter);
  }

  public JsonNode toJson() {
    validate();

    var copy = new LinkedHashMap<>(props);
    if (!paint.isEmpty()) {
      copy.put("paint", paint);
    }
    if (!filter.isEmpty()) {
      copy.put("filter", filter);
    }
    if (!layout.isEmpty()) {
      copy.put("layout", layout);
    }
    if (!line.isEmpty()) {
      copy.put("line", line);
    }
    return OBJECT_MAPPER.valueToTree(copy);
  }

  private StyleBuilder filterClasses(Class... classToFilter) {
    var clazzes = Arrays.stream(classToFilter).map(Class::getSimpleName).toList();
    filter = ListUtils.combine(List.of("in", "class"), clazzes);
    return this;
  }

  private String validateColor(String color) {
    if (!color.startsWith("#")) {
      throw new IllegalArgumentException("Colors must start with '#'");
    }
    return color;
  }

  private void validate() {
    Stream
      .of(TYPE)
      .forEach(p -> Objects.requireNonNull(props.get(p), "%s must be set".formatted(p)));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy