Skip to content

Commit

Permalink
Merge pull request #5673 from ibi-group/vector-attribution
Browse files Browse the repository at this point in the history
Allow configuring a custom attribution for vector tiles
  • Loading branch information
leonardehrenfried authored Feb 15, 2024
2 parents ad2cf7a + e55c2c9 commit 1c95460
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 21 deletions.
17 changes: 17 additions & 0 deletions docs/sandbox/MapboxVectorTilesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ For each layer, the configuration includes:

| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since |
|----------------------------------------------------------------|:----------:|--------------------------------------------------------------------------------------------|:----------:|---------------|:-----:|
| [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 |
Expand All @@ -161,6 +162,22 @@ For each layer, the configuration includes:

#### Details

<h4 id="vectorTiles_attribution">attribution</h4>

**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
including HTML tags,
for example `<a href='https://trimet.org/mod'>Regional Partners</a>`.


<h4 id="vectorTiles_basePath">basePath</h4>

**Since version:** `2.5`**Type:** `string`**Cardinality:** `Optional`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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<String> rLayers = Arrays.asList(requestedLayers.split(","));

Expand All @@ -101,7 +95,24 @@ 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(() -> {
var feedInfos = getFeedInfos();
return new TileJson(url, envelope, feedInfos);
});
}

private List<FeedInfo> getFeedInfos() {
return serverContext
.transitService()
.getFeedIds()
.stream()
.map(serverContext.transitService()::getFeedInfo)
.filter(Predicate.not(Objects::isNull))
.toList();
}

private static LayerBuilder<?> crateLayerBuilder(
Expand Down
25 changes: 16 additions & 9 deletions src/main/java/org/opentripplanner/apis/support/TileJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,8 @@ public class TileJson implements Serializable {
public final double[] bounds;
public final double[] center;

public TileJson(String tileUrl, WorldEnvelope envelope, Collection<FeedInfo> feedInfos) {
attribution =
feedInfos
.stream()
.map(feedInfo ->
"<a href='" + feedInfo.getPublisherUrl() + "'>" + feedInfo.getPublisherName() + "</a>"
)
.collect(Collectors.joining(", "));

public TileJson(String tileUrl, WorldEnvelope envelope, String attribution) {
this.attribution = attribution;
tiles = new String[] { tileUrl };

bounds =
Expand All @@ -59,6 +52,10 @@ public TileJson(String tileUrl, WorldEnvelope envelope, Collection<FeedInfo> fee
center = new double[] { c.longitude(), c.latitude(), 9 };
}

public TileJson(String tileUrl, WorldEnvelope envelope, Collection<FeedInfo> feedInfos) {
this(tileUrl, envelope, attributionFromFeedInfo(feedInfos));
}

/**
* Creates a vector source layer URL from a hard-coded path plus information from the incoming
* HTTP request.
Expand Down Expand Up @@ -96,4 +93,14 @@ public static String urlFromOverriddenBasePath(
String.join(",", layers)
);
}

private static String attributionFromFeedInfo(Collection<FeedInfo> feedInfos) {
return feedInfos
.stream()
.map(feedInfo ->
"<a href='%s'>%s</a>".formatted(feedInfo.getPublisherUrl(), feedInfo.getPublisherName())
)
.distinct()
.collect(Collectors.joining(", "));
}
}
11 changes: 10 additions & 1 deletion src/main/java/org/opentripplanner/framework/io/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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).findFirst().get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,23 @@
public class VectorTileConfig
implements VectorTilesResource.LayersParameters<VectorTilesResource.LayerType> {

public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null);
public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null, null);
private final List<LayerParameters<VectorTilesResource.LayerType>> layers;

@Nullable
private final String basePath;

@Nullable
private final String attribution;

VectorTileConfig(
Collection<? extends LayerParameters<VectorTilesResource.LayerType>> layers,
@Nullable String basePath
@Nullable String basePath,
@Nullable String attribution
) {
this.layers = List.copyOf(layers);
this.basePath = basePath;
this.attribution = attribution;
}

@Override
Expand All @@ -41,6 +46,10 @@ public Optional<String> basePath() {
return Optional.ofNullable(basePath);
}

public Optional<String> 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(
Expand Down Expand Up @@ -71,7 +80,23 @@ 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("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
including HTML tags,
for example `<a href='https://trimet.org/mod'>Regional Partners</a>`.
"""
)
.asString(DEFAULT.attribution)
);
}

Expand Down
36 changes: 36 additions & 0 deletions src/test/java/org/opentripplanner/apis/support/TileJsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,33 @@

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<String> LAYERS = List.of("stops", "rentalVehicles");
private static final WorldEnvelope ENVELOPE = WorldEnvelope
.of()
.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(
Expand Down Expand Up @@ -40,4 +57,23 @@ void defaultPath() {
TileJson.urlWithDefaultPath(uriInfo, req, LAYERS, "default", "vectorTiles")
);
}

@Test
void attributionFromFeedInfo() {
var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO));
assertEquals("<a href='https://trimet.org'>Trimet</a>", tileJson.attribution);
}

@Test
void duplicateAttribution() {
var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO, FEED_INFO));
assertEquals("<a href='https://trimet.org'>Trimet</a>", tileJson.attribution);
}

@Test
void attributionFromOverride() {
var override = "OVERRIDE";
var tileJson = new TileJson("http://example.com", ENVELOPE, override);
assertEquals(override, tileJson.attribution);
}
}
38 changes: 38 additions & 0 deletions src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}

0 comments on commit 1c95460

Please sign in to comment.