diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index f27d252f250..7070f8b486e 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -4,7 +4,6 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.opentripplanner.apis.vectortiles.model.StyleBuilder; import org.opentripplanner.apis.vectortiles.model.StyleSpec; @@ -14,6 +13,7 @@ import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber; import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber.ZoomStop; import org.opentripplanner.service.vehiclerental.street.StreetVehicleRentalLink; +import org.opentripplanner.standalone.config.debuguiconfig.BackgroundTileLayer; import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; @@ -37,13 +37,25 @@ */ public class DebugStyleSpec { - private static final TileSource BACKGROUND_SOURCE = new RasterSource( - "background", + private static final TileSource OSM_BACKGROUND = new RasterSource( + "OSM Carto", List.of("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"), 19, 256, "© OpenStreetMap Contributors" ); + private static final TileSource POSITRON_BACKGROUND = new RasterSource( + "Positron", + List.of("https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}{ratio}.png"), + 19, + 256, + "© OpenStreetMap, © CARTO" + ); + + private static final List BACKGROUND_LAYERS = List.of( + OSM_BACKGROUND, + POSITRON_BACKGROUND + ); private static final String MAGENTA = "#f21d52"; private static final String BRIGHT_GREEN = "#22DD9E"; private static final String DARK_GREEN = "#136b04"; @@ -92,19 +104,33 @@ static StyleSpec build( VectorSourceLayer areaStops, VectorSourceLayer groupStops, VectorSourceLayer edges, - VectorSourceLayer vertices + VectorSourceLayer vertices, + List extraLayers ) { - var vectorSources = Stream + List vectorSources = Stream .of(regularStops, edges, vertices) - .map(VectorSourceLayer::vectorSource); - var allSources = Stream - .concat(Stream.of(BACKGROUND_SOURCE), vectorSources) - .collect(Collectors.toSet()); + .map(VectorSourceLayer::vectorSource) + .map(TileSource.class::cast) + .toList(); + + List extraRasterSources = extraLayers + .stream() + .map(l -> + (TileSource) new RasterSource( + l.name(), + List.of(l.templateUrl()), + 19, + l.tileSize(), + l.attribution() + ) + ) + .toList(); + var allSources = ListUtils.combine(BACKGROUND_LAYERS, extraRasterSources, vectorSources); return new StyleSpec( "OTP Debug Tiles", allSources, ListUtils.combine( - List.of(StyleBuilder.ofId("background").typeRaster().source(BACKGROUND_SOURCE).minZoom(0)), + backgroundLayers(extraRasterSources), wheelchair(edges), noThruTraffic(edges), traversalPermissions(edges), @@ -115,6 +141,25 @@ static StyleSpec build( ); } + private static List backgroundLayers(List extraLayers) { + return ListUtils + .combine(BACKGROUND_LAYERS, extraLayers) + .stream() + .map(layer -> { + var builder = StyleBuilder + .ofId(layer.id()) + .displayName(layer.name()) + .typeRaster() + .source(layer) + .minZoom(0); + if (!layer.equals(OSM_BACKGROUND)) { + builder.intiallyHidden(); + } + return builder; + }) + .toList(); + } + private static List stops( VectorSourceLayer regularStops, VectorSourceLayer areaStops, diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java index d19c25a1f47..0576e91f312 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java @@ -147,7 +147,8 @@ public StyleSpec getTileJson(@Context UriInfo uri, @Context HttpHeaders headers) AREA_STOPS.toVectorSourceLayer(stopsSource), GROUP_STOPS.toVectorSourceLayer(stopsSource), EDGES.toVectorSourceLayer(streetSource), - VERTICES.toVectorSourceLayer(streetSource) + VERTICES.toVectorSourceLayer(streetSource), + serverContext.debugUiConfig().additionalBackgroundLayers() ); } diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java index a70acbb8685..4e75f37785d 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java @@ -30,7 +30,7 @@ public class StyleBuilder { private final Map layout = new LinkedHashMap<>(); private final Map metadata = new LinkedHashMap<>(); private final Map line = new LinkedHashMap<>(); - private List filter = List.of(); + private List filter = List.of(); public static StyleBuilder ofId(String id) { return new StyleBuilder(id); @@ -120,6 +120,14 @@ public StyleBuilder group(String group) { return this; } + /** + * A nice human-readable name for the layer. + */ + public StyleBuilder displayName(String name) { + metadata.put("name", name); + return this; + } + public StyleBuilder lineText(String name) { layout.put("symbol-placement", "line-center"); layout.put("symbol-spacing", 1000); diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/TileSource.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/TileSource.java index 088ce63d10a..b7f0ce5d048 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/TileSource.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/TileSource.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.List; +import org.opentripplanner.utils.lang.StringUtils; /** * Represent a data source where Maplibre can fetch data for rendering directly in the browser. @@ -12,6 +13,8 @@ public sealed interface TileSource { String id(); + String name(); + /** * Represents a vector tile source which is rendered into a map in the browser. */ @@ -20,17 +23,32 @@ record VectorSource(String id, String url) implements TileSource { public String type() { return "vector"; } + + @Override + public String name() { + return id; + } } /** * Represents a raster-based source for map tiles. These are used mainly for background * map layers with vector data being rendered on top of it. */ - record RasterSource(String id, List tiles, int maxzoom, int tileSize, String attribution) + record RasterSource( + String name, + List tiles, + int maxzoom, + int tileSize, + String attribution + ) implements TileSource { @Override public String type() { return "raster"; } + + public String id() { + return StringUtils.slugify(name); + } } } diff --git a/application/src/main/java/org/opentripplanner/framework/application/OtpFileNames.java b/application/src/main/java/org/opentripplanner/framework/application/OtpFileNames.java index 7b6852cb281..6c3a5b6a007 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OtpFileNames.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OtpFileNames.java @@ -9,16 +9,18 @@ public class OtpFileNames { public static final String OTP_CONFIG_FILENAME = "otp-config.json"; public static final String BUILD_CONFIG_FILENAME = "build-config.json"; public static final String ROUTER_CONFIG_FILENAME = "router-config.json"; + public static final String DEBUG_UI_CONFIG_FILENAME = "debug-ui-config.json"; /** * Check if a file is a config file using the configuration file name. This method returns {@code - * true} if the file match {@code (otp|build|router)-config.json}. + * true} if the file match {@code (otp|build|router|debug-ui)-config.json}. */ public static boolean isConfigFile(String filename) { return ( OTP_CONFIG_FILENAME.equals(filename) || BUILD_CONFIG_FILENAME.equals(filename) || - ROUTER_CONFIG_FILENAME.equals(filename) + ROUTER_CONFIG_FILENAME.equals(filename) || + DEBUG_UI_CONFIG_FILENAME.equals(filename) ); } } diff --git a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index b5b39ddee18..f088a3de60e 100644 --- a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -26,6 +26,7 @@ import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.search.state.State; @@ -127,6 +128,8 @@ default GraphFinder graphFinder() { VectorTileConfig vectorTileConfig(); + DebugUiConfig debugUiConfig(); + /* Sandbox modules */ @Nullable diff --git a/application/src/main/java/org/opentripplanner/standalone/config/ConfigModel.java b/application/src/main/java/org/opentripplanner/standalone/config/ConfigModel.java index c82dd33ddbf..390333f1374 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/ConfigModel.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/ConfigModel.java @@ -49,16 +49,29 @@ public class ConfigModel { */ private RouterConfig routerConfig; - public ConfigModel(OtpConfig otpConfig, BuildConfig buildConfig, RouterConfig routerConfig) { + private final DebugUiConfig debugUiConfig; + + public ConfigModel( + OtpConfig otpConfig, + BuildConfig buildConfig, + RouterConfig routerConfig, + DebugUiConfig debugUiConfig + ) { this.otpConfig = otpConfig; this.buildConfig = buildConfig; this.routerConfig = routerConfig; + this.debugUiConfig = debugUiConfig; initializeOtpFeatures(otpConfig); } public ConfigModel(OtpConfigLoader loader) { - this(loader.loadOtpConfig(), loader.loadBuildConfig(), loader.loadRouterConfig()); + this( + loader.loadOtpConfig(), + loader.loadBuildConfig(), + loader.loadRouterConfig(), + loader.loadDebugUiConfig() + ); } public void updateConfigFromSerializedGraph(BuildConfig buildConfig, RouterConfig routerConfig) { @@ -102,6 +115,10 @@ public RouterConfig routerConfig() { return routerConfig; } + public DebugUiConfig debugUiConfig() { + return debugUiConfig; + } + public static void initializeOtpFeatures(OtpConfig otpConfig) { OTPFeature.enableFeatures(otpConfig.otpFeatures); OTPFeature.logFeatureSetup(); @@ -117,7 +134,8 @@ public void abortOnUnknownParameters() { ( otpConfig.hasUnknownParameters() || buildConfig.hasUnknownParameters() || - routerConfig.hasUnknownParameters() + routerConfig.hasUnknownParameters() || + debugUiConfig.hasUnknownParameters() ) ) { throw new OtpAppException( diff --git a/application/src/main/java/org/opentripplanner/standalone/config/DebugUiConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/DebugUiConfig.java new file mode 100644 index 00000000000..cca6d2359be --- /dev/null +++ b/application/src/main/java/org/opentripplanner/standalone/config/DebugUiConfig.java @@ -0,0 +1,106 @@ +package org.opentripplanner.standalone.config; + +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import java.util.List; +import org.opentripplanner.standalone.config.debuguiconfig.BackgroundTileLayer; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is an object representation of the 'debug-ui-config.json'. + */ +public class DebugUiConfig { + + private static final Logger LOG = LoggerFactory.getLogger(DebugUiConfig.class); + + public static final DebugUiConfig DEFAULT = new DebugUiConfig( + MissingNode.getInstance(), + "DEFAULT", + false + ); + + /** + * The node adaptor kept for reference and (de)serialization. + */ + private final NodeAdapter root; + private final List additionalBackgroundLayers; + + public DebugUiConfig(JsonNode node, String source, boolean logUnusedParams) { + this(new NodeAdapter(node, source), logUnusedParams); + } + + /** protected to give unit-test access */ + DebugUiConfig(NodeAdapter root, boolean logUnusedParams) { + this.root = root; + + this.additionalBackgroundLayers = + root + .of("additionalBackgroundLayers") + .since(V2_7) + .summary("Additional background raster map layers.") + .description( + """ + Add additional background layers that will appear in the Debug UI as one of the choices. + + Currently only raster tile layers are supported. + """ + ) + .asObjects( + List.of(), + node -> + new BackgroundTileLayer( + node + .of("name") + .since(V2_7) + .summary("Name to appear in the layer selector.") + .asString(), + node + .of("templateUrl") + .since(V2_7) + .summary( + """ + The [Maplibre-compatible template URL](https://maplibre.org/maplibre-native/ios/api/tile-url-templates.html) + for the raster layer, for example `https://examples.com/tiles/{z}/{x}/{y}.png`. + """ + ) + .asString(), + node.of("tileSize").since(V2_7).summary("Size of the tile in pixels.").asInt(256), + node + .of("attribution") + .since(V2_7) + .summary("Attribution for the map data.") + .asString("© OpenTripPlanner") + ) + ); + + if (logUnusedParams) { + root.logAllWarnings(LOG::warn); + } + } + + public NodeAdapter asNodeAdapter() { + return root; + } + + public List additionalBackgroundLayers() { + return additionalBackgroundLayers; + } + + /** + * If {@code true} the config is loaded from file, in not the DEFAULT config is used. + */ + public boolean isDefault() { + return root.isEmpty(); + } + + /** + * Checks if any unknown or invalid parameters were encountered while loading the configuration. + */ + public boolean hasUnknownParameters() { + return root.hasUnknownParameters(); + } +} diff --git a/application/src/main/java/org/opentripplanner/standalone/config/OtpConfigLoader.java b/application/src/main/java/org/opentripplanner/standalone/config/OtpConfigLoader.java index d5c452b3da7..9e24e56ca12 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/OtpConfigLoader.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/OtpConfigLoader.java @@ -1,6 +1,7 @@ package org.opentripplanner.standalone.config; import static org.opentripplanner.framework.application.OtpFileNames.BUILD_CONFIG_FILENAME; +import static org.opentripplanner.framework.application.OtpFileNames.DEBUG_UI_CONFIG_FILENAME; import static org.opentripplanner.framework.application.OtpFileNames.OTP_CONFIG_FILENAME; import static org.opentripplanner.framework.application.OtpFileNames.ROUTER_CONFIG_FILENAME; @@ -105,6 +106,14 @@ public RouterConfig loadRouterConfig() { return new RouterConfig(node, ROUTER_CONFIG_FILENAME, true); } + public DebugUiConfig loadDebugUiConfig() { + JsonNode node = loadFromFile(DEBUG_UI_CONFIG_FILENAME); + if (node.isMissingNode()) { + return DebugUiConfig.DEFAULT; + } + return new DebugUiConfig(node, DEBUG_UI_CONFIG_FILENAME, true); + } + private static void logConfigVersion(String configVersion, String filename) { if (configVersion != null) { LOG.info("{} config-version is {}.", filename, configVersion); diff --git a/application/src/main/java/org/opentripplanner/standalone/config/configure/ConfigModule.java b/application/src/main/java/org/opentripplanner/standalone/config/configure/ConfigModule.java index 4f75f3984d5..9a4de91f2ab 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/configure/ConfigModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/configure/ConfigModule.java @@ -8,6 +8,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.standalone.config.ConfigModel; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.OtpConfig; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.config.routerconfig.RaptorEnvironmentFactory; @@ -34,6 +35,11 @@ static RouterConfig provideRouterConfig(ConfigModel model) { return model.routerConfig(); } + @Provides + static DebugUiConfig provideDebugUiConfig(ConfigModel model) { + return model.debugUiConfig(); + } + @Provides @Singleton static RaptorConfig providesRaptorConfig( diff --git a/application/src/main/java/org/opentripplanner/standalone/config/debuguiconfig/BackgroundTileLayer.java b/application/src/main/java/org/opentripplanner/standalone/config/debuguiconfig/BackgroundTileLayer.java new file mode 100644 index 00000000000..121debcb40e --- /dev/null +++ b/application/src/main/java/org/opentripplanner/standalone/config/debuguiconfig/BackgroundTileLayer.java @@ -0,0 +1,8 @@ +package org.opentripplanner.standalone.config.debuguiconfig; + +public record BackgroundTileLayer( + String name, + String templateUrl, + int tileSize, + String attribution +) {} diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index eb3fae5275f..b4edbb36299 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -27,6 +27,7 @@ import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.standalone.config.CommandLineParameters; import org.opentripplanner.standalone.config.ConfigModel; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.OtpConfig; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.server.GrizzlyServer; @@ -300,6 +301,10 @@ public BuildConfig buildConfig() { return factory.config().buildConfig(); } + public DebugUiConfig debugUiConfig() { + return factory.config().debugUiConfig(); + } + public RaptorConfig raptorConfig() { return factory.raptorConfig(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index bbdd39c57d5..42bd8ee4d87 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -20,6 +20,7 @@ import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.server.DefaultServerRequestContext; import org.opentripplanner.street.service.StreetLimitationParametersService; @@ -32,6 +33,7 @@ public class ConstructApplicationModule { @Provides OtpServerRequestContext providesServerContext( RouterConfig routerConfig, + DebugUiConfig debugUiConfig, RaptorConfig raptorConfig, Graph graph, TransitService transitService, @@ -69,7 +71,8 @@ OtpServerRequestContext providesServerContext( stopConsolidationService, streetLimitationParametersService, traverseVisitor, - luceneIndex + luceneIndex, + debugUiConfig ); } diff --git a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 1427dcf1971..450b6986cb5 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -26,6 +26,7 @@ import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.api.HttpRequestScoped; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.routerconfig.TransitRoutingConfig; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; import org.opentripplanner.street.service.StreetLimitationParametersService; @@ -57,6 +58,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final StopConsolidationService stopConsolidationService; private final StreetLimitationParametersService streetLimitationParametersService; private final LuceneIndex luceneIndex; + private final DebugUiConfig debugUiConfig; private RouteRequest defaultRouteRequestWithTimeSet = null; @@ -83,7 +85,8 @@ private DefaultServerRequestContext( StreetLimitationParametersService streetLimitationParametersService, FlexParameters flexParameters, @Nullable TraverseVisitor traverseVisitor, - @Nullable LuceneIndex luceneIndex + @Nullable LuceneIndex luceneIndex, + DebugUiConfig debugUiConfig ) { this.graph = graph; this.transitService = transitService; @@ -105,6 +108,7 @@ private DefaultServerRequestContext( this.stopConsolidationService = stopConsolidationService; this.streetLimitationParametersService = streetLimitationParametersService; this.luceneIndex = luceneIndex; + this.debugUiConfig = debugUiConfig; } /** @@ -129,7 +133,8 @@ public static DefaultServerRequestContext create( @Nullable StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, @Nullable TraverseVisitor traverseVisitor, - @Nullable LuceneIndex luceneIndex + @Nullable LuceneIndex luceneIndex, + DebugUiConfig debugUiConfig ) { return new DefaultServerRequestContext( graph, @@ -151,7 +156,8 @@ public static DefaultServerRequestContext create( streetLimitationParametersService, flexParameters, traverseVisitor, - luceneIndex + luceneIndex, + debugUiConfig ); } @@ -262,6 +268,11 @@ public VectorTileConfig vectorTileConfig() { return vectorTileConfig; } + @Override + public DebugUiConfig debugUiConfig() { + return debugUiConfig; + } + @Nullable @Override public LuceneIndex lucenceIndex() { diff --git a/application/src/test/java/org/opentripplanner/TestServerContext.java b/application/src/test/java/org/opentripplanner/TestServerContext.java index e20720bd7d8..ca818a64a58 100644 --- a/application/src/test/java/org/opentripplanner/TestServerContext.java +++ b/application/src/test/java/org/opentripplanner/TestServerContext.java @@ -21,6 +21,7 @@ import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeService; import org.opentripplanner.service.worldenvelope.model.WorldEnvelope; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.config.routerconfig.RaptorEnvironmentFactory; import org.opentripplanner.standalone.server.DefaultServerRequestContext; @@ -65,7 +66,8 @@ public static OtpServerRequestContext createServerContext( null, createStreetLimitationParametersService(), null, - null + null, + DebugUiConfig.DEFAULT ); creatTransitLayerForRaptor(timetableRepository, routerConfig.transitTuningConfig()); return context; diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index b3c64e0aecb..dc96a094812 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -52,6 +52,7 @@ import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeRepository; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeService; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.server.DefaultServerRequestContext; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -154,7 +155,8 @@ void setup() { null, new DefaultStreetLimitationParametersService(new StreetLimitationParameters()), null, - null + null, + DebugUiConfig.DEFAULT ), null, transitService diff --git a/application/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java b/application/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java index db056050394..26d1044047e 100644 --- a/application/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java +++ b/application/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.apis.vectortiles.model.TileSource.VectorSource; import org.opentripplanner.apis.vectortiles.model.VectorSourceLayer; @@ -27,7 +28,14 @@ void spec() throws IOException { var groupStops = new VectorSourceLayer(vectorSource, "stops"); var edges = new VectorSourceLayer(vectorSource, "edges"); var vertices = new VectorSourceLayer(vectorSource, "vertices"); - var spec = DebugStyleSpec.build(regularStops, areaStops, groupStops, edges, vertices); + var spec = DebugStyleSpec.build( + regularStops, + areaStops, + groupStops, + edges, + vertices, + List.of() + ); var json = ObjectMappers.ignoringExtraFields().valueToTree(spec); try { diff --git a/application/src/test/java/org/opentripplanner/generate/doc/DebugUiConfigurationDocTest.java b/application/src/test/java/org/opentripplanner/generate/doc/DebugUiConfigurationDocTest.java new file mode 100644 index 00000000000..a5124c0d3e0 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/generate/doc/DebugUiConfigurationDocTest.java @@ -0,0 +1,67 @@ +package org.opentripplanner.generate.doc; + +import static org.opentripplanner.framework.io.FileUtils.assertFileEquals; +import static org.opentripplanner.framework.io.FileUtils.readFile; +import static org.opentripplanner.framework.io.FileUtils.writeFile; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.TEMPLATE_PATH; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.USER_DOC_PATH; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceJsonExample; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceParametersDetails; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceParametersTable; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.jsonNodeFromResource; +import static org.opentripplanner.utils.text.MarkdownFormatter.HEADER_3; + +import java.io.File; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.application.OtpFileNames; +import org.opentripplanner.generate.doc.framework.GeneratesDocumentation; +import org.opentripplanner.generate.doc.framework.ParameterDetailsList; +import org.opentripplanner.generate.doc.framework.ParameterSummaryTable; +import org.opentripplanner.generate.doc.framework.SkipNodes; +import org.opentripplanner.standalone.config.DebugUiConfig; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; + +@GeneratesDocumentation +public class DebugUiConfigurationDocTest { + + private static final String CONFIG_JSON = OtpFileNames.DEBUG_UI_CONFIG_FILENAME; + private static final File TEMPLATE = new File(TEMPLATE_PATH, "DebugUiConfiguration.md"); + private static final File OUT_FILE = new File(USER_DOC_PATH, "DebugUiConfiguration.md"); + + private static final String CONFIG_PATH = "standalone/config/" + CONFIG_JSON; + + /** + * NOTE! This test updates the {@code doc/user/Configuration.md} document based on the latest + * version of the code. + */ + @Test + public void updateDoc() { + NodeAdapter node = readConfig(); + + // Read and close input file (same as output file) + String doc = readFile(TEMPLATE); + String original = readFile(OUT_FILE); + + doc = replaceParametersTable(doc, getParameterSummaryTable(node)); + doc = replaceParametersDetails(doc, getParameterDetailsTable(node)); + doc = replaceJsonExample(doc, node, CONFIG_JSON); + + writeFile(OUT_FILE, doc); + + assertFileEquals(original, OUT_FILE); + } + + private NodeAdapter readConfig() { + var json = jsonNodeFromResource(CONFIG_PATH); + var conf = new DebugUiConfig(json, CONFIG_PATH, true); + return conf.asNodeAdapter(); + } + + private String getParameterSummaryTable(NodeAdapter node) { + return new ParameterSummaryTable(SkipNodes.of().build()).createTable(node).toMarkdownTable(); + } + + private String getParameterDetailsTable(NodeAdapter node) { + return ParameterDetailsList.listParametersWithDetails(node, SkipNodes.of().build(), HEADER_3); + } +} diff --git a/application/src/test/java/org/opentripplanner/standalone/config/ExampleConfigTest.java b/application/src/test/java/org/opentripplanner/standalone/config/ExampleConfigTest.java index 06b0d8ed592..934da6f923d 100644 --- a/application/src/test/java/org/opentripplanner/standalone/config/ExampleConfigTest.java +++ b/application/src/test/java/org/opentripplanner/standalone/config/ExampleConfigTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.opentripplanner.framework.application.OtpFileNames.BUILD_CONFIG_FILENAME; +import static org.opentripplanner.framework.application.OtpFileNames.DEBUG_UI_CONFIG_FILENAME; import static org.opentripplanner.framework.application.OtpFileNames.OTP_CONFIG_FILENAME; import static org.opentripplanner.framework.application.OtpFileNames.ROUTER_CONFIG_FILENAME; @@ -60,6 +61,17 @@ void otpConfig(Path filename) { testConfig(filename, nodeAdapter -> new OtpConfig(nodeAdapter, true)); } + @FilePatternSource( + pattern = { + "doc/user/examples/**/" + DEBUG_UI_CONFIG_FILENAME, + "application/src/test/resources/standalone/config/" + DEBUG_UI_CONFIG_FILENAME, + } + ) + @ParameterizedTest(name = "Check validity of {0}") + void debugUiConfig(Path filename) { + testConfig(filename, nodeAdapter -> new DebugUiConfig(nodeAdapter, true)); + } + @FilePatternSource( pattern = { "application/src/test/resources/standalone/config/invalid-config.json" } ) diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index c55ddff854f..ca4e85eed84 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -29,6 +29,7 @@ import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.standalone.config.ConfigModel; +import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.OtpConfigLoader; import org.opentripplanner.standalone.config.routerconfig.RaptorEnvironmentFactory; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; @@ -130,7 +131,8 @@ public SpeedTest( null, TestServerContext.createStreetLimitationParametersService(), null, - null + null, + DebugUiConfig.DEFAULT ); // Creating transitLayerForRaptor should be integrated into the TimetableRepository, but for now // we do it manually here diff --git a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index b08a225fde3..66858390ab5 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -1,28 +1,52 @@ { "name": "OTP Debug Tiles", "sources": { - "background": { - "id": "background", - "tiles": ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"], + "positron": { + "name": "Positron", + "tiles": [ + "https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}{ratio}.png" + ], "maxzoom": 19, "tileSize": 256, - "attribution": "© OpenStreetMap Contributors", + "attribution": "© OpenStreetMap, © CARTO", "type": "raster" }, "vectorSource": { "id": "vectorSource", "url": "https://example.com", "type": "vector" + }, + "osm-carto": { + "name": "OSM Carto", + "tiles": ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"], + "maxzoom": 19, + "tileSize": 256, + "attribution": "© OpenStreetMap Contributors", + "type": "raster" } }, "layers": [ { - "id": "background", + "id": "osm-carto", "type": "raster", - "source": "background", + "source": "osm-carto", "minzoom": 0, "metadata": { - "group": "Other" + "group": "Other", + "name": "OSM Carto" + } + }, + { + "id": "positron", + "type": "raster", + "source": "positron", + "minzoom": 0, + "layout": { + "visibility": "none" + }, + "metadata": { + "group": "Other", + "name": "Positron" } }, { diff --git a/application/src/test/resources/standalone/config/debug-ui-config.json b/application/src/test/resources/standalone/config/debug-ui-config.json new file mode 100644 index 00000000000..1ae690a4d37 --- /dev/null +++ b/application/src/test/resources/standalone/config/debug-ui-config.json @@ -0,0 +1,9 @@ +{ + "additionalBackgroundLayers": [ + { + "name": "TriMet aerial photos", + "templateUrl": "https://maps.trimet.org/wms/reflect?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.0&request=GetMap&srs=EPSG:3857&width=256&height=256&layers=aerials", + "attribution": "© TriMet" + } + ] +} diff --git a/client/index.html b/client/index.html index 77eb289e595..f09832636f0 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - OTP Debug Client + OTP Debug
diff --git a/client/src/components/MapView/LayerControl.tsx b/client/src/components/MapView/LayerControl.tsx index 61dd48e4a34..a41e7f133a4 100644 --- a/client/src/components/MapView/LayerControl.tsx +++ b/client/src/components/MapView/LayerControl.tsx @@ -26,49 +26,93 @@ class LayerControl implements IControl { this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group layer-select'; map.on('load', () => { - // clean on + // clean on rerender while (this.container.firstChild) { this.container.removeChild(this.container.firstChild); } - const title = document.createElement('h4'); - title.textContent = 'Debug layers'; - this.container.appendChild(title); - - const groups: Record = {}; - map - .getLayersOrder() - .map((l) => map.getLayer(l)) - .filter((layer) => !!layer) - .filter((layer) => this.layerInteractive(layer)) - .reverse() - .forEach((layer) => { - if (layer) { - const meta: { group: string } = layer.metadata as { group: string }; - - let groupName: string = 'Misc'; - if (meta.group) { - groupName = meta.group; - } - - const layerDiv = this.buildLayerDiv(layer as TypedStyleLayer, map); - - if (groups[groupName]) { - groups[groupName]?.appendChild(layerDiv); - } else { - const groupDiv = this.buildGroupDiv(groupName, layerDiv); - groups[groupName] = groupDiv; - this.container.appendChild(groupDiv); - } - } - }); - // initialize clickable layers (initially stops) - this.updateInteractiveLayerIds(map); + this.buildBackgroundLayers(map); + this.buildDebugLayers(map); }); return this.container; } + private buildBackgroundLayers(map: Map) { + const title = document.createElement('h4'); + title.textContent = 'Background'; + this.container.appendChild(title); + + const select = document.createElement('select'); + this.container.appendChild(select); + + const rasterLayers = map + .getLayersOrder() + .map((l) => map.getLayer(l)) + .filter((layer) => !!layer) + .filter((layer) => layer?.type == 'raster'); + + rasterLayers.forEach((layer) => { + if (layer) { + const option = document.createElement('option'); + const meta = layer.metadata as { name: string }; + option.textContent = meta.name; + option.id = layer.id; + option.value = layer.id; + + select.appendChild(option); + } + }); + select.onchange = () => { + const layerId = select.value; + console.log(select.value); + const layer = map.getLayer(layerId); + if (layer) { + rasterLayers.forEach((l) => { + map.setLayoutProperty(l?.id, 'visibility', 'none'); + }); + + map.setLayoutProperty(layer.id, 'visibility', 'visible'); + } + }; + } + + private buildDebugLayers(map: Map) { + const title = document.createElement('h4'); + title.textContent = 'Debug layers'; + this.container.appendChild(title); + + const groups: Record = {}; + map + .getLayersOrder() + .map((l) => map.getLayer(l)) + .filter((layer) => !!layer) + .filter((layer) => this.layerInteractive(layer)) + .reverse() + .forEach((layer) => { + if (layer) { + const meta: { group: string } = layer.metadata as { group: string }; + + let groupName: string = 'Misc'; + if (meta.group) { + groupName = meta.group; + } + + const layerDiv = this.buildLayerDiv(layer as TypedStyleLayer, map); + + if (groups[groupName]) { + groups[groupName]?.appendChild(layerDiv); + } else { + const groupDiv = this.buildGroupDiv(groupName, layerDiv); + groups[groupName] = groupDiv; + this.container.appendChild(groupDiv); + } + } + }); + // initialize clickable layers (initially stops) + this.updateInteractiveLayerIds(map); + } + private updateInteractiveLayerIds(map: Map) { const visibleInteractiveLayerIds = map .getLayersOrder() diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 47781532179..e90a54eab80 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -34,7 +34,7 @@ export function SearchBar({ onRoute, tripQueryVariables, setTripQueryVariables,
setShowServerInfo((v) => !v)}>
- OTP Debug Client + OTP Debug {showServerInfo && }
diff --git a/client/src/style.css b/client/src/style.css index c33f5dff711..f838dce2c7b 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -181,6 +181,10 @@ margin-left: 6px; } +.maplibregl-ctrl-group.layer-select select { + margin-bottom: 14px; +} + .maplibregl-ctrl-group.layer-select h4 { font-size: 17px; } diff --git a/doc/templates/Configuration.md b/doc/templates/Configuration.md index 45b2c36c67b..39843b57077 100644 --- a/doc/templates/Configuration.md +++ b/doc/templates/Configuration.md @@ -41,9 +41,9 @@ default behavior of scanning the base directory for input files. Scanning is ove independently for each file type, and can point to remote cloud storage with arbitrary URIs. See [the storage section](Configuration.md#Storage) for further details. -## Three Scopes of Configuration +## Four scopes of configuration -OTP is configured via three configuration JSON files which are read from the directory specified on +OTP is configured via four configuration JSON files which are read from the directory specified on its command line. We try to provide sensible defaults for every option, so all three of these files are optional, as are all the options within each file. Each configuration file corresponds to options that are relevant at a particular phase of OTP usage. @@ -51,7 +51,8 @@ options that are relevant at a particular phase of OTP usage. Options and parameters that are taken into account during the graph building process will be "baked into" the graph, and cannot be changed later in a running server. These are specified in `build-config.json`. Other details of OTP operation can be modified without rebuilding the graph. -These run-time configuration options are found in `router-config.json`. Finally, `otp-config.json` +These run-time configuration options are found in `router-config.json`. If you want to configure +the built-in debug UI add `debug-ui-config.json`. Finally, `otp-config.json` contains simple switches that enable or disable system-wide features. ## Configuration types diff --git a/doc/templates/DebugUiConfiguration.md b/doc/templates/DebugUiConfiguration.md new file mode 100644 index 00000000000..a54b1db8d2a --- /dev/null +++ b/doc/templates/DebugUiConfiguration.md @@ -0,0 +1,23 @@ + + +# Debug UI configuration + +The Debug UI is the standard interface that is bundled with OTP and available by visiting +[`http://localhost:8080`](http://localhost:8080). This page list the configuration options available +by placing a file `debug-ui-config.json` into OTP's working directory. + + + + +## Parameter Details + + + +## Config Example + + diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index 13ba16889b1..97c70594b78 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -41,9 +41,9 @@ default behavior of scanning the base directory for input files. Scanning is ove independently for each file type, and can point to remote cloud storage with arbitrary URIs. See [the storage section](Configuration.md#Storage) for further details. -## Three Scopes of Configuration +## Four scopes of configuration -OTP is configured via three configuration JSON files which are read from the directory specified on +OTP is configured via four configuration JSON files which are read from the directory specified on its command line. We try to provide sensible defaults for every option, so all three of these files are optional, as are all the options within each file. Each configuration file corresponds to options that are relevant at a particular phase of OTP usage. @@ -51,7 +51,8 @@ options that are relevant at a particular phase of OTP usage. Options and parameters that are taken into account during the graph building process will be "baked into" the graph, and cannot be changed later in a running server. These are specified in `build-config.json`. Other details of OTP operation can be modified without rebuilding the graph. -These run-time configuration options are found in `router-config.json`. Finally, `otp-config.json` +These run-time configuration options are found in `router-config.json`. If you want to configure +the built-in debug UI add `debug-ui-config.json`. Finally, `otp-config.json` contains simple switches that enable or disable system-wide features. ## Configuration types diff --git a/doc/user/DebugUiConfiguration.md b/doc/user/DebugUiConfiguration.md new file mode 100644 index 00000000000..a1657796fe0 --- /dev/null +++ b/doc/user/DebugUiConfiguration.md @@ -0,0 +1,66 @@ + + +# Debug UI configuration + +The Debug UI is the standard interface that is bundled with OTP and available by visiting +[`http://localhost:8080`](http://localhost:8080). This page list the configuration options available +by placing a file `debug-ui-config.json` into OTP's working directory. + + + + +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|-----------------------------------------------------------|:----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|-----------------------|:-----:| +| [additionalBackgroundLayers](#additionalBackgroundLayers) | `object[]` | Additional background raster map layers. | *Optional* | | 2.7 | +|       attribution | `string` | Attribution for the map data. | *Optional* | `"© OpenTripPlanner"` | 2.7 | +|       name | `string` | Name to appear in the layer selector. | *Required* | | 2.7 | +|       templateUrl | `string` | The [Maplibre-compatible template URL](https://maplibre.org/maplibre-native/ios/api/tile-url-templates.html) for the raster layer, for example `https://examples.com/tiles/{z}/{x}/{y}.png`. | *Required* | | 2.7 | +|       tileSize | `integer` | Size of the tile in pixels. | *Optional* | `256` | 2.7 | + + + + +## Parameter Details + + + + +

additionalBackgroundLayers

+ +**Since version:** `2.7` ∙ **Type:** `object[]` ∙ **Cardinality:** `Optional` +**Path:** / + +Additional background raster map layers. + +Add additional background layers that will appear in the Debug UI as one of the choices. + +Currently only raster tile layers are supported. + + + + + +## Config Example + + + + +```JSON +// debug-ui-config.json +{ + "additionalBackgroundLayers" : [ + { + "name" : "TriMet aerial photos", + "templateUrl" : "https://maps.trimet.org/wms/reflect?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.0&request=GetMap&srs=EPSG:3857&width=256&height=256&layers=aerials", + "attribution" : "© TriMet" + } + ] +} +``` + + diff --git a/mkdocs.yml b/mkdocs.yml index b40f77ff3c0..51245f6c3b8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,7 @@ nav: - Router: 'RouterConfiguration.md' - "Route Request": 'RouteRequest.md' - "Realtime Updaters": 'UpdaterConfig.md' + - "Debug UI": 'DebugUiConfiguration.md' - "Migrating between versions/builds": 'Migrating-Configuration.md' - Features explained: - "Routing modes": 'RoutingModes.md' diff --git a/utils/src/main/java/org/opentripplanner/utils/lang/StringUtils.java b/utils/src/main/java/org/opentripplanner/utils/lang/StringUtils.java index 72eb2638c13..c1a2219da72 100644 --- a/utils/src/main/java/org/opentripplanner/utils/lang/StringUtils.java +++ b/utils/src/main/java/org/opentripplanner/utils/lang/StringUtils.java @@ -132,6 +132,14 @@ public static String kebabCase(String input) { return input.toLowerCase().replace('_', '-'); } + /** + * Create a URL-friendly "slug" version of the string, so "Entur Routebanken" becomes + * "entur-routebanken". + */ + public static String slugify(String input) { + return input.toLowerCase().replace('_', '-').replaceAll("\\s+", "-"); + } + /** * Detects unprintable control characters like newlines, tabs and invisible whitespace * like 'ZERO WIDTH SPACE' (U+200B) that don't have an immediate visual representation. diff --git a/utils/src/test/java/org/opentripplanner/utils/lang/StringUtilsTest.java b/utils/src/test/java/org/opentripplanner/utils/lang/StringUtilsTest.java index 7e8f0a6217b..dda9248468e 100644 --- a/utils/src/test/java/org/opentripplanner/utils/lang/StringUtilsTest.java +++ b/utils/src/test/java/org/opentripplanner/utils/lang/StringUtilsTest.java @@ -100,4 +100,10 @@ void containsInvisibleChars(String input) { void noInvisibleChars(String input) { assertFalse(StringUtils.containsInvisibleCharacters(input)); } + + @ParameterizedTest + @ValueSource(strings = { "AAA Bbb", "aAa bbb", "aaa bbb", "aaa bbb", "AAA_BBB" }) + void slugify(String input) { + assertEquals("aaa-bbb", StringUtils.slugify(input)); + } }