From 056d7585fb86eebb6a6456e259fc9cf63885a646 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 7 Feb 2024 14:11:32 +0100 Subject: [PATCH 1/5] Allow configuring a custom attribution for vector tiles --- docs/sandbox/MapboxVectorTilesApi.md | 1 + .../ext/vectortiles/VectorTilesResource.java | 6 +++- .../apis/support/TileJson.java | 27 +++++++++++------ .../config/routerconfig/VectorTileConfig.java | 20 +++++++++++-- .../apis/support/TileJsonTest.java | 30 +++++++++++++++++++ 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index da9fd1120e1..8e4af296155 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -148,6 +148,7 @@ For each layer, the configuration includes: | Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | |----------------------------------------------------------------|:----------:|--------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| attribution | `string` | Set a custom attribution to be returned in `tilejson.json` | *Optional* | | 2.5 | | [basePath](#vectorTiles_basePath) | `string` | The path of the vector tile source URLs in `tilejson.json`. | *Optional* | | 2.5 | | [layers](#vectorTiles_layers) | `object[]` | Configuration of the individual layers for the Mapbox vector tiles. | *Optional* | | 2.0 | |       type = "stop" | `enum` | Type of the layer. | *Required* | | 2.0 | diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java index 772db7394f3..b4e59dc49e5 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java @@ -101,7 +101,11 @@ public TileJson getTileJson( TileJson.urlWithDefaultPath(uri, headers, rLayers, ignoreRouterId, "vectorTiles") ); - return new TileJson(url, envelope, feedInfos); + return serverContext + .vectorTileConfig() + .attribution() + .map(attr -> new TileJson(url, envelope, attr)) + .orElseGet(() -> new TileJson(url, envelope, feedInfos)); } private static LayerBuilder crateLayerBuilder( diff --git a/src/main/java/org/opentripplanner/apis/support/TileJson.java b/src/main/java/org/opentripplanner/apis/support/TileJson.java index 75aabb2b6c6..72fe1ac3140 100644 --- a/src/main/java/org/opentripplanner/apis/support/TileJson.java +++ b/src/main/java/org/opentripplanner/apis/support/TileJson.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.opentripplanner.framework.io.HttpUtils; import org.opentripplanner.model.FeedInfo; @@ -36,15 +37,8 @@ public class TileJson implements Serializable { public final double[] bounds; public final double[] center; - public TileJson(String tileUrl, WorldEnvelope envelope, Collection feedInfos) { - attribution = - feedInfos - .stream() - .map(feedInfo -> - "" + feedInfo.getPublisherName() + "" - ) - .collect(Collectors.joining(", ")); - + public TileJson(String tileUrl, WorldEnvelope envelope, String attribution) { + this.attribution = attribution; tiles = new String[] { tileUrl }; bounds = @@ -59,6 +53,21 @@ public TileJson(String tileUrl, WorldEnvelope envelope, Collection fee center = new double[] { c.longitude(), c.latitude(), 9 }; } + public TileJson(String tileUrl, WorldEnvelope envelope, Collection feedInfos) { + this(tileUrl, envelope, attributionFromFeedInfo(feedInfos)); + } + + @Nonnull + private static String attributionFromFeedInfo(Collection feedInfos) { + var attribution = feedInfos + .stream() + .map(feedInfo -> + "" + feedInfo.getPublisherName() + "" + ) + .collect(Collectors.joining(", ")); + return attribution; + } + /** * Creates a vector source layer URL from a hard-coded path plus information from the incoming * HTTP request. diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index 6f7d6967ce8..4afc14a4ea7 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -18,18 +18,23 @@ public class VectorTileConfig implements VectorTilesResource.LayersParameters { - public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null); + public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null, null); private final List> layers; @Nullable private final String basePath; + @Nullable + private final String attribution; + VectorTileConfig( Collection> layers, - @Nullable String basePath + @Nullable String basePath, + @Nullable String attribution ) { this.layers = List.copyOf(layers); this.basePath = basePath; + this.attribution = attribution; } @Override @@ -41,6 +46,10 @@ public Optional basePath() { return Optional.ofNullable(basePath); } + public Optional attribution() { + return Optional.ofNullable(attribution); + } + public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String paramName) { var root = node.of(paramName).summary("Vector tile configuration").asObject(); return new VectorTileConfig( @@ -71,7 +80,12 @@ public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String is expected to be handled by a proxy. """ ) - .asString(DEFAULT.basePath) + .asString(DEFAULT.basePath), + root + .of("attribution") + .since(V2_5) + .summary("Set a custom attribution to be returned in `tilejson.json`") + .asString(DEFAULT.attribution) ); } diff --git a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java index ac3b7bca522..f5f0401cf86 100644 --- a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java +++ b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java @@ -2,16 +2,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.LocalDate; import java.util.List; import org.glassfish.jersey.server.internal.routing.UriRoutingContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.model.FeedInfo; +import org.opentripplanner.service.worldenvelope.model.WorldEnvelope; import org.opentripplanner.test.support.HttpForTest; class TileJsonTest { private static final List LAYERS = List.of("stops", "rentalVehicles"); + private static final WorldEnvelope ENVELOPE = WorldEnvelope + .of() + .expandToIncludeStreetEntities(1, 1) + .expandToIncludeStreetEntities(2, 2) + .build(); @ParameterizedTest @ValueSource( @@ -40,4 +48,26 @@ void defaultPath() { TileJson.urlWithDefaultPath(uriInfo, req, LAYERS, "default", "vectorTiles") ); } + + @Test + void attributionFromFeedInfo() { + var feedInfo = new FeedInfo( + "1", + "Trimet", + "https://trimet.org", + "en", + LocalDate.MIN, + LocalDate.MIN, + "1" + ); + var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(feedInfo)); + assertEquals("Trimet", tileJson.attribution); + } + + @Test + void attributionFromOverride() { + final String override = "OVERRIDE"; + var tileJson = new TileJson("http://example.com", ENVELOPE, override); + assertEquals(override, tileJson.attribution); + } } From c7e8e0d60e39b3cd44723f4809cfa2dae5c86fbe Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 7 Feb 2024 16:38:05 +0100 Subject: [PATCH 2/5] Refactor attribution handling --- .../ext/vectortiles/VectorTilesResource.java | 23 +++++++++------ .../apis/support/TileJson.java | 22 +++++++-------- .../apis/support/TileJsonTest.java | 28 +++++++++++-------- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java index b4e59dc49e5..a1e9a83c85a 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java @@ -28,6 +28,7 @@ import org.opentripplanner.inspector.vector.LayerBuilder; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.inspector.vector.VectorTileResponseFactory; +import org.opentripplanner.model.FeedInfo; import org.opentripplanner.standalone.api.OtpServerRequestContext; @Path("/routers/{ignoreRouterId}/vectorTiles") @@ -81,13 +82,6 @@ public TileJson getTileJson( @PathParam("layers") String requestedLayers ) { var envelope = serverContext.worldEnvelopeService().envelope().orElseThrow(); - var feedInfos = serverContext - .transitService() - .getFeedIds() - .stream() - .map(serverContext.transitService()::getFeedInfo) - .filter(Predicate.not(Objects::isNull)) - .toList(); List rLayers = Arrays.asList(requestedLayers.split(",")); @@ -105,7 +99,20 @@ public TileJson getTileJson( .vectorTileConfig() .attribution() .map(attr -> new TileJson(url, envelope, attr)) - .orElseGet(() -> new TileJson(url, envelope, feedInfos)); + .orElseGet(() -> { + var feedInfos = getFeedInfos(); + return new TileJson(url, envelope, feedInfos); + }); + } + + private List getFeedInfos() { + return serverContext + .transitService() + .getFeedIds() + .stream() + .map(serverContext.transitService()::getFeedInfo) + .filter(Predicate.not(Objects::isNull)) + .toList(); } private static LayerBuilder crateLayerBuilder( diff --git a/src/main/java/org/opentripplanner/apis/support/TileJson.java b/src/main/java/org/opentripplanner/apis/support/TileJson.java index 72fe1ac3140..fcd6e84cc06 100644 --- a/src/main/java/org/opentripplanner/apis/support/TileJson.java +++ b/src/main/java/org/opentripplanner/apis/support/TileJson.java @@ -6,7 +6,6 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.opentripplanner.framework.io.HttpUtils; import org.opentripplanner.model.FeedInfo; @@ -57,17 +56,6 @@ public TileJson(String tileUrl, WorldEnvelope envelope, Collection fee this(tileUrl, envelope, attributionFromFeedInfo(feedInfos)); } - @Nonnull - private static String attributionFromFeedInfo(Collection feedInfos) { - var attribution = feedInfos - .stream() - .map(feedInfo -> - "" + feedInfo.getPublisherName() + "" - ) - .collect(Collectors.joining(", ")); - return attribution; - } - /** * Creates a vector source layer URL from a hard-coded path plus information from the incoming * HTTP request. @@ -105,4 +93,14 @@ public static String urlFromOverriddenBasePath( String.join(",", layers) ); } + + private static String attributionFromFeedInfo(Collection feedInfos) { + return feedInfos + .stream() + .map(feedInfo -> + "" + feedInfo.getPublisherName() + "" + ) + .distinct() + .collect(Collectors.joining(", ")); + } } diff --git a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java index f5f0401cf86..73c8cd1369e 100644 --- a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java +++ b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java @@ -20,6 +20,15 @@ class TileJsonTest { .expandToIncludeStreetEntities(1, 1) .expandToIncludeStreetEntities(2, 2) .build(); + private static final FeedInfo FEED_INFO = new FeedInfo( + "1", + "Trimet", + "https://trimet.org", + "en", + LocalDate.MIN, + LocalDate.MIN, + "1" + ); @ParameterizedTest @ValueSource( @@ -51,22 +60,19 @@ void defaultPath() { @Test void attributionFromFeedInfo() { - var feedInfo = new FeedInfo( - "1", - "Trimet", - "https://trimet.org", - "en", - LocalDate.MIN, - LocalDate.MIN, - "1" - ); - var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(feedInfo)); + var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO)); + assertEquals("Trimet", tileJson.attribution); + } + + @Test + void duplicateAttribution() { + var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO, FEED_INFO)); assertEquals("Trimet", tileJson.attribution); } @Test void attributionFromOverride() { - final String override = "OVERRIDE"; + var override = "OVERRIDE"; var tileJson = new TileJson("http://example.com", ENVELOPE, override); assertEquals(override, tileJson.attribution); } From db5eb9e915a0e463fd4593df7e111cea977a1e39 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 7 Feb 2024 16:48:43 +0100 Subject: [PATCH 3/5] Flesh out documentation --- docs/sandbox/MapboxVectorTilesApi.md | 17 ++++++++++++++++- .../config/routerconfig/VectorTileConfig.java | 12 +++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index 8e4af296155..11bd195a9d8 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -148,7 +148,7 @@ For each layer, the configuration includes: | Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | |----------------------------------------------------------------|:----------:|--------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| -| attribution | `string` | Set a custom attribution to be returned in `tilejson.json` | *Optional* | | 2.5 | +| [attribution](#vectorTiles_attribution) | `string` | Custom attribution to be returned in `tilejson.json` | *Optional* | | 2.5 | | [basePath](#vectorTiles_basePath) | `string` | The path of the vector tile source URLs in `tilejson.json`. | *Optional* | | 2.5 | | [layers](#vectorTiles_layers) | `object[]` | Configuration of the individual layers for the Mapbox vector tiles. | *Optional* | | 2.0 | |       type = "stop" | `enum` | Type of the layer. | *Required* | | 2.0 | @@ -162,6 +162,21 @@ For each layer, the configuration includes: #### Details +

attribution

+ +**Since version:** `2.5` ∙ **Type:** `string` ∙ **Cardinality:** `Optional` +**Path:** /vectorTiles + +Custom attribution to be returned in `tilejson.json` + +By default the `attribution` property in `tilejson.json` is computed from the names and +URLs of the feed publishers. +If the OTP deployment contains many fields, this can become very unwieldy. + +This configuration parameter allows you to set the `attribution` to any string you wish, +for example `TriMet, C-Tran, SMART and Friends`. + +

basePath

**Since version:** `2.5` ∙ **Type:** `string` ∙ **Cardinality:** `Optional` diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index 4afc14a4ea7..f0c6cb7d9b0 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -84,7 +84,17 @@ public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String root .of("attribution") .since(V2_5) - .summary("Set a custom attribution to be returned in `tilejson.json`") + .summary("Custom attribution to be returned in `tilejson.json`") + .description( + """ + By default the `attribution` property in `tilejson.json` is computed from the names and + URLs of the feed publishers. + If the OTP deployment contains many fields, this can become very unwieldy. + + This configuration parameter allows you to set the `attribution` to any string you wish, + for example `TriMet, C-Tran, SMART and Friends`. + """ + ) .asString(DEFAULT.attribution) ); } From 840ab9ce823acfda7529f3ec1f2898b48560bc74 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 14 Feb 2024 10:10:03 +0100 Subject: [PATCH 4/5] Account for a comma-separated list of X-Forwarded-Host --- .../framework/io/HttpUtils.java | 11 +++++- .../framework/io/HttpUtilsTest.java | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java diff --git a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java index 4981a8ab91b..d8b3dc542a6 100644 --- a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java +++ b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java @@ -3,6 +3,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.UriInfo; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import org.apache.hc.core5.http.ContentType; public final class HttpUtils { @@ -31,7 +32,7 @@ public static String getBaseAddress(UriInfo uri, HttpHeaders headers) { String host; if (headers.getRequestHeader(HEADER_X_FORWARDED_HOST) != null) { - host = headers.getRequestHeader(HEADER_X_FORWARDED_HOST).getFirst(); + host = extractHost(headers.getRequestHeader(HEADER_X_FORWARDED_HOST).getFirst()); } else if (headers.getRequestHeader(HEADER_HOST) != null) { host = headers.getRequestHeader(HEADER_HOST).getFirst(); } else { @@ -40,4 +41,12 @@ public static String getBaseAddress(UriInfo uri, HttpHeaders headers) { return protocol + "://" + host; } + + /** + * The X-Forwarded-Host header can contain a comma-separated list so we account for that. + * https://stackoverflow.com/questions/66042952/http-proxy-behavior-for-x-forwarded-host-header + */ + private static String extractHost(String xForwardedFor) { + return Arrays.stream(xForwardedFor.split(",")).map(String::strip).toList().getFirst(); + } } diff --git a/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java b/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java new file mode 100644 index 00000000000..ac8ae52848c --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.framework.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; +import org.glassfish.jersey.server.internal.routing.UriRoutingContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.test.support.HttpForTest; + +class HttpUtilsTest { + + private static final String HOSTNAME = "example.com"; + + static List testCases() { + return List.of( + HOSTNAME, + "example.com,", + " example.com ,", + "example.com,example.com", + "example.com, example.com", + "example.com, example.net", + "example.com, example.net, example.com", + " example.com, example.net, example.com" + ); + } + + @ParameterizedTest + @MethodSource("testCases") + void extractHost(String headerValue) { + var req = HttpForTest.containerRequest(); + req.headers(Map.of("X-Forwarded-Host", List.of(headerValue))); + var uriInfo = new UriRoutingContext(req); + var baseAddress = HttpUtils.getBaseAddress(uriInfo, req); + assertEquals("https://" + HOSTNAME, baseAddress); + } +} From e55c2c91d234de98f15dde60401823bf051f23ff Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 14 Feb 2024 17:37:19 +0100 Subject: [PATCH 5/5] Apply review feedback --- docs/sandbox/MapboxVectorTilesApi.md | 7 ++++--- .../java/org/opentripplanner/apis/support/TileJson.java | 2 +- .../java/org/opentripplanner/framework/io/HttpUtils.java | 2 +- .../standalone/config/routerconfig/VectorTileConfig.java | 7 ++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index 11bd195a9d8..7183bdf8b7f 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -169,12 +169,13 @@ For each layer, the configuration includes: Custom attribution to be returned in `tilejson.json` -By default the `attribution` property in `tilejson.json` is computed from the names and +By default the, `attribution` property in `tilejson.json` is computed from the names and URLs of the feed publishers. If the OTP deployment contains many fields, this can become very unwieldy. -This configuration parameter allows you to set the `attribution` to any string you wish, -for example `TriMet, C-Tran, SMART and Friends`. +This configuration parameter allows you to set the `attribution` to any string you wish +including HTML tags, +for example `Regional Partners`.

basePath

diff --git a/src/main/java/org/opentripplanner/apis/support/TileJson.java b/src/main/java/org/opentripplanner/apis/support/TileJson.java index fcd6e84cc06..1f5c090c16d 100644 --- a/src/main/java/org/opentripplanner/apis/support/TileJson.java +++ b/src/main/java/org/opentripplanner/apis/support/TileJson.java @@ -98,7 +98,7 @@ private static String attributionFromFeedInfo(Collection feedInfos) { return feedInfos .stream() .map(feedInfo -> - "" + feedInfo.getPublisherName() + "" + "%s".formatted(feedInfo.getPublisherUrl(), feedInfo.getPublisherName()) ) .distinct() .collect(Collectors.joining(", ")); diff --git a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java index d8b3dc542a6..672c9b9c481 100644 --- a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java +++ b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java @@ -47,6 +47,6 @@ public static String getBaseAddress(UriInfo uri, HttpHeaders headers) { * https://stackoverflow.com/questions/66042952/http-proxy-behavior-for-x-forwarded-host-header */ private static String extractHost(String xForwardedFor) { - return Arrays.stream(xForwardedFor.split(",")).map(String::strip).toList().getFirst(); + return Arrays.stream(xForwardedFor.split(",")).map(String::strip).findFirst().get(); } } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index f0c6cb7d9b0..d6beac6d256 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -87,12 +87,13 @@ public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String .summary("Custom attribution to be returned in `tilejson.json`") .description( """ - By default the `attribution` property in `tilejson.json` is computed from the names and + By default the, `attribution` property in `tilejson.json` is computed from the names and URLs of the feed publishers. If the OTP deployment contains many fields, this can become very unwieldy. - This configuration parameter allows you to set the `attribution` to any string you wish, - for example `TriMet, C-Tran, SMART and Friends`. + This configuration parameter allows you to set the `attribution` to any string you wish + including HTML tags, + for example `Regional Partners`. """ ) .asString(DEFAULT.attribution)