MapSnapshot attribution (#10362)
* [android] - add attribution * [android] - optimise attribution sources * [android] - rework datamodel to attribution class * [android] - refactor Attribution, add tests * [android] - add getter for attribution string * [android] - rework attribution to include small logo, add layout placement * [android] - finalise integration and layout logic
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package com.mapbox.mapboxsdk.attribution;
|
||||
|
||||
public class Attribution {
|
||||
|
||||
private static final String OPENSTREETMAP = "OpenStreetMap";
|
||||
private static final String OPENSTREETMAP_ABBR = "OSM";
|
||||
static final String TELEMETRY = "Telemetry Settings";
|
||||
|
||||
static final String IMPROVE_MAP_URL = "https://www.mapbox.com/map-feedback/";
|
||||
static final String MAPBOX_URL = "https://www.mapbox.com/about/maps/";
|
||||
static final String TELEMETRY_URL = "https://www.mapbox.com/telemetry/";
|
||||
|
||||
private String title;
|
||||
private String url;
|
||||
|
||||
Attribution(String title, String url) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getTitleAbbreviated() {
|
||||
if (title.equals(OPENSTREETMAP)) {
|
||||
return OPENSTREETMAP_ABBR;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Attribution that = (Attribution) o;
|
||||
|
||||
if (title != null ? !title.equals(that.title) : that.title != null) {
|
||||
return false;
|
||||
}
|
||||
return url != null ? url.equals(that.url) : that.url == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = title != null ? title.hashCode() : 0;
|
||||
result = 31 * result + (url != null ? url.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.mapbox.mapboxsdk.attribution;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class AttributionLayout {
|
||||
|
||||
private Bitmap logo;
|
||||
private PointF anchorPoint;
|
||||
private boolean shortText;
|
||||
|
||||
public AttributionLayout(@Nullable Bitmap logo, @Nullable PointF anchorPoint, boolean shortText) {
|
||||
this.logo = logo;
|
||||
this.anchorPoint = anchorPoint;
|
||||
this.shortText = shortText;
|
||||
}
|
||||
|
||||
public Bitmap getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
public PointF getAnchorPoint() {
|
||||
return anchorPoint;
|
||||
}
|
||||
|
||||
public boolean isShortText() {
|
||||
return shortText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AttributionLayout that = (AttributionLayout) o;
|
||||
|
||||
if (logo != null ? !logo.equals(that.logo) : that.logo != null) {
|
||||
return false;
|
||||
}
|
||||
return anchorPoint != null ? anchorPoint.equals(that.anchorPoint) : that.anchorPoint == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = logo != null ? logo.hashCode() : 0;
|
||||
result = 31 * result + (anchorPoint != null ? anchorPoint.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AttributionLayout{"
|
||||
+ "logo=" + logo
|
||||
+ ", anchorPoint=" + anchorPoint
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.mapbox.mapboxsdk.attribution;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AttributionMeasure {
|
||||
|
||||
private Bitmap logo;
|
||||
private Bitmap logoSmall;
|
||||
private Bitmap snapshot;
|
||||
private TextView textView;
|
||||
private TextView textViewShort;
|
||||
private float margin;
|
||||
|
||||
private boolean shorterText;
|
||||
|
||||
AttributionMeasure(Bitmap snapshot, Bitmap logo, Bitmap logoSmall, TextView tv, TextView tvShort, float margin) {
|
||||
this.snapshot = snapshot;
|
||||
this.logo = logo;
|
||||
this.logoSmall = logoSmall;
|
||||
this.textView = tv;
|
||||
this.textViewShort = tvShort;
|
||||
this.margin = margin;
|
||||
}
|
||||
|
||||
public AttributionLayout measure() {
|
||||
Chain chain = new Chain(
|
||||
new FullLogoLongTextCommand(),
|
||||
new FullLogoShortTextCommand(),
|
||||
new SmallLogoLongTextCommand(),
|
||||
new SmallLogoShortTextCommand(),
|
||||
new LongTextCommand(),
|
||||
new ShortTextCommand(),
|
||||
new NoTextCommand()
|
||||
);
|
||||
|
||||
AttributionLayout attributionLayout = chain.start(this);
|
||||
shorterText = attributionLayout.isShortText();
|
||||
return attributionLayout;
|
||||
}
|
||||
|
||||
|
||||
private static class FullLogoLongTextCommand implements Command {
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin);
|
||||
return new AttributionLayout(measure.logo, anchor, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FullLogoShortTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin);
|
||||
return new AttributionLayout(measure.logo, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SmallLogoLongTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getLogoSmallContainerWidth() + measure.getTextViewContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin);
|
||||
return new AttributionLayout(measure.logoSmall, anchor, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SmallLogoShortTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth();
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin);
|
||||
return new AttributionLayout(measure.logoSmall, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LongTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getTextViewContainerWidth() + measure.margin;
|
||||
boolean fitBounds = width <= measure.getMaxSize();
|
||||
if (fitBounds) {
|
||||
return new AttributionLayout(null, calculateAnchor(measure.snapshot, measure.textView, measure.margin), false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ShortTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
float width = measure.getTextViewShortContainerWidth() + measure.margin;
|
||||
boolean fitBounds = width <= measure.getMaxSizeShort();
|
||||
if (fitBounds) {
|
||||
PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin);
|
||||
return new AttributionLayout(null, anchor, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoTextCommand implements Command {
|
||||
@Override
|
||||
public AttributionLayout execute(AttributionMeasure measure) {
|
||||
return new AttributionLayout(null, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static PointF calculateAnchor(Bitmap snapshot, TextView textView, float margin) {
|
||||
return new PointF(
|
||||
snapshot.getWidth() - textView.getMeasuredWidth() - margin,
|
||||
snapshot.getHeight() - margin - textView.getMeasuredHeight()
|
||||
);
|
||||
}
|
||||
|
||||
public TextView getTextView() {
|
||||
return shorterText ? textViewShort : textView;
|
||||
}
|
||||
|
||||
private class Chain {
|
||||
public List<Command> commands;
|
||||
|
||||
Chain(Command... commands) {
|
||||
this.commands = Arrays.asList(commands);
|
||||
}
|
||||
|
||||
public AttributionLayout start(AttributionMeasure measure) {
|
||||
AttributionLayout attributionLayout = null;
|
||||
for (Command command : commands) {
|
||||
attributionLayout = command.execute(measure);
|
||||
if (attributionLayout != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return attributionLayout;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Command {
|
||||
AttributionLayout execute(AttributionMeasure measure);
|
||||
}
|
||||
|
||||
private float getTextViewContainerWidth() {
|
||||
return textView.getMeasuredWidth() + margin;
|
||||
}
|
||||
|
||||
private float getLogoContainerWidth() {
|
||||
return logo.getWidth() + (2 * margin);
|
||||
}
|
||||
|
||||
private float getTextViewShortContainerWidth() {
|
||||
return textViewShort.getMeasuredWidth() + margin;
|
||||
}
|
||||
|
||||
private float getLogoSmallContainerWidth() {
|
||||
return logoSmall.getWidth() + (2 * margin);
|
||||
}
|
||||
|
||||
private float getMaxSize() {
|
||||
return snapshot.getWidth() * 8 / 10;
|
||||
}
|
||||
|
||||
private float getMaxSizeShort() {
|
||||
return snapshot.getWidth();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Bitmap snapshot;
|
||||
private Bitmap logo;
|
||||
private Bitmap logoSmall;
|
||||
private TextView textView;
|
||||
private TextView textViewShort;
|
||||
private float marginPadding;
|
||||
|
||||
public Builder setSnapshot(Bitmap snapshot) {
|
||||
this.snapshot = snapshot;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLogo(Bitmap logo) {
|
||||
this.logo = logo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLogoSmall(Bitmap logoSmall) {
|
||||
this.logoSmall = logoSmall;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTextView(TextView textView) {
|
||||
this.textView = textView;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTextViewShort(TextView textViewShort) {
|
||||
this.textViewShort = textViewShort;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMarginPadding(float marginPadding) {
|
||||
this.marginPadding = marginPadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributionMeasure build() {
|
||||
return new AttributionMeasure(snapshot, logo, logoSmall, textView, textViewShort, marginPadding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package com.mapbox.mapboxsdk.attribution;
|
||||
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Responsible for parsing attribution data coming from Sources and MapSnapshot.
|
||||
* <p>
|
||||
* Exposes multiple configuration options to manipulate data being parsed.
|
||||
* Use the Options object to build these configurations.
|
||||
* </p>
|
||||
*/
|
||||
public class AttributionParser {
|
||||
|
||||
private final Set<Attribution> attributions = new LinkedHashSet<>();
|
||||
private final String attributionData;
|
||||
private final boolean withImproveMap;
|
||||
private final boolean withCopyrightSign;
|
||||
private final boolean withTelemetryAttribution;
|
||||
private final boolean withMapboxAttribution;
|
||||
|
||||
AttributionParser(String attributionData, boolean withImproveMap, boolean withCopyrightSign,
|
||||
boolean withTelemetryAttribution, boolean withMapboxAttribution) {
|
||||
this.attributionData = attributionData;
|
||||
this.withImproveMap = withImproveMap;
|
||||
this.withCopyrightSign = withCopyrightSign;
|
||||
this.withTelemetryAttribution = withTelemetryAttribution;
|
||||
this.withMapboxAttribution = withMapboxAttribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attributions.
|
||||
*
|
||||
* @return the attributions
|
||||
*/
|
||||
public Set<Attribution> getAttributions() {
|
||||
return attributions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attribution string.
|
||||
*
|
||||
* @return the parsed attribution string
|
||||
*/
|
||||
public String createAttributionString() {
|
||||
return createAttributionString(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed attribution string.
|
||||
*
|
||||
* @param shortenedOutput if attribution string should contain shortened output
|
||||
* @return the parsed attribution string
|
||||
*/
|
||||
public String createAttributionString(boolean shortenedOutput) {
|
||||
StringBuilder stringBuilder = new StringBuilder(withCopyrightSign ? "" : "© ");
|
||||
int counter = 0;
|
||||
for (Attribution attribution : attributions) {
|
||||
counter++;
|
||||
stringBuilder.append(!shortenedOutput ? attribution.getTitle() : attribution.getTitleAbbreviated());
|
||||
if (counter != attributions.size()) {
|
||||
stringBuilder.append(" / ");
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main attribution for configuration
|
||||
*/
|
||||
protected void parse() {
|
||||
parseAttributions();
|
||||
addAdditionalAttributions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse attributions
|
||||
*/
|
||||
private void parseAttributions() {
|
||||
SpannableStringBuilder htmlBuilder = (SpannableStringBuilder) fromHtml(attributionData);
|
||||
URLSpan[] urlSpans = htmlBuilder.getSpans(0, htmlBuilder.length(), URLSpan.class);
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
parseUrlSpan(htmlBuilder, urlSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an URLSpan containing an attribution.
|
||||
*
|
||||
* @param htmlBuilder the html builder
|
||||
* @param urlSpan the url span to be parsed
|
||||
*/
|
||||
private void parseUrlSpan(SpannableStringBuilder htmlBuilder, URLSpan urlSpan) {
|
||||
String url = urlSpan.getURL();
|
||||
if (isUrlValid(url)) {
|
||||
String anchor = parseAnchorValue(htmlBuilder, urlSpan);
|
||||
attributions.add(new Attribution(anchor, url));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid to be included in the final attribution.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid
|
||||
*/
|
||||
private boolean isUrlValid(String url) {
|
||||
return isValidForImproveThisMap(url) && isValidForMapbox(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid for the improve map configuration.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid for improve this map
|
||||
*/
|
||||
private boolean isValidForImproveThisMap(String url) {
|
||||
return withImproveMap || !url.equals(Attribution.IMPROVE_MAP_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to validate if an url is valid for the Mapbox configuration.
|
||||
*
|
||||
* @param url the url to be validated
|
||||
* @return if the url is valid for Mapbox
|
||||
*/
|
||||
private boolean isValidForMapbox(String url) {
|
||||
return withMapboxAttribution || !url.equals(Attribution.MAPBOX_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the attribution by parsing the anchor value of html href tag.
|
||||
*
|
||||
* @param htmlBuilder the html builder
|
||||
* @param urlSpan the current urlSpan
|
||||
* @return the parsed anchor value
|
||||
*/
|
||||
private String parseAnchorValue(SpannableStringBuilder htmlBuilder, URLSpan urlSpan) {
|
||||
int start = htmlBuilder.getSpanStart(urlSpan);
|
||||
int end = htmlBuilder.getSpanEnd(urlSpan);
|
||||
int length = end - start;
|
||||
char[] charKey = new char[length];
|
||||
htmlBuilder.getChars(start, end, charKey, 0);
|
||||
return stripCopyright(String.valueOf(charKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to strip the copyright sign from an attribution
|
||||
*
|
||||
* @param anchor the attribution string to strip
|
||||
* @return the stripped attribution string without the copyright sign
|
||||
*/
|
||||
private String stripCopyright(String anchor) {
|
||||
if (!withCopyrightSign && anchor.startsWith("© ")) {
|
||||
anchor = anchor.substring(2, anchor.length());
|
||||
}
|
||||
return anchor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to manually add attributions
|
||||
*/
|
||||
private void addAdditionalAttributions() {
|
||||
if (withTelemetryAttribution) {
|
||||
attributions.add(new Attribution(Attribution.TELEMETRY, Attribution.TELEMETRY_URL));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to a spanned html representation.
|
||||
*
|
||||
* @param html the string to convert
|
||||
* @return the spanned html representation
|
||||
*/
|
||||
private static Spanned fromHtml(String html) {
|
||||
Spanned result;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
result = Html.fromHtml(html);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to configure using an AttributionParser.
|
||||
* <p>
|
||||
* AttributionData, set with {@link #withAttributionData(String...)}, is the only required property to build
|
||||
* the underlying AttributionParser. Other properties include trimming the copyright sign, adding telemetry
|
||||
* attribution or hiding attribution as improve this map and Mapbox.
|
||||
* </p>
|
||||
*/
|
||||
public static class Options {
|
||||
private boolean withImproveMap = true;
|
||||
private boolean withCopyrightSign = true;
|
||||
private boolean withTelemetryAttribution = false;
|
||||
private boolean withMapboxAttribution = true;
|
||||
private String[] attributionDataStringArray;
|
||||
|
||||
public Options withAttributionData(String... attributionData) {
|
||||
this.attributionDataStringArray = attributionData;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options withImproveMap(boolean withImproveMap) {
|
||||
this.withImproveMap = withImproveMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options withCopyrightSign(boolean withCopyrightSign) {
|
||||
this.withCopyrightSign = withCopyrightSign;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options withTelemetryAttribution(boolean withTelemetryAttribution) {
|
||||
this.withTelemetryAttribution = withTelemetryAttribution;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options withMapboxAttribution(boolean withMapboxAttribution) {
|
||||
this.withMapboxAttribution = withMapboxAttribution;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributionParser build() {
|
||||
if (attributionDataStringArray == null) {
|
||||
throw new IllegalStateException("Using builder without providing attribution data");
|
||||
}
|
||||
|
||||
String fullAttributionString = parseAttribution(attributionDataStringArray);
|
||||
AttributionParser attributionParser = new AttributionParser(
|
||||
fullAttributionString,
|
||||
withImproveMap,
|
||||
withCopyrightSign,
|
||||
withTelemetryAttribution,
|
||||
withMapboxAttribution
|
||||
);
|
||||
attributionParser.parse();
|
||||
return attributionParser;
|
||||
}
|
||||
|
||||
private String parseAttribution(String[] attribution) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String attr : attribution) {
|
||||
if (!attr.isEmpty()) {
|
||||
builder.append(attr);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ class HTTPRequest implements Callback {
|
||||
private HTTPRequest(long nativePtr, String resourceUrl, String etag, String modified) {
|
||||
mNativePtr = nativePtr;
|
||||
|
||||
Timber.e("requesting: %s",resourceUrl);
|
||||
try {
|
||||
HttpUrl httpUrl = HttpUrl.parse(resourceUrl);
|
||||
final String host = httpUrl.host().toLowerCase(MapboxConstants.MAPBOX_LOCALE);
|
||||
|
||||
@@ -7,22 +7,20 @@ import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mapbox.mapboxsdk.R;
|
||||
import com.mapbox.mapboxsdk.attribution.Attribution;
|
||||
import com.mapbox.mapboxsdk.attribution.AttributionParser;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import com.mapbox.mapboxsdk.style.sources.Source;
|
||||
import com.mapbox.services.android.telemetry.MapboxTelemetry;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Responsible for managing attribution interactions on the map.
|
||||
@@ -39,8 +37,8 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface.
|
||||
|
||||
private final Context context;
|
||||
private final MapboxMap mapboxMap;
|
||||
private String[] attributionKeys;
|
||||
private HashMap<String, String> attributionMap;
|
||||
private String[] attributionTitles;
|
||||
private Set<Attribution> attributionSet;
|
||||
|
||||
AttributionDialogManager(@NonNull Context context, @NonNull MapboxMap mapboxMap) {
|
||||
this.context = context;
|
||||
@@ -50,18 +48,26 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface.
|
||||
// Called when someone presses the attribution icon on the map
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
attributionMap = new AttributionBuilder(context, mapboxMap).build();
|
||||
attributionSet = new AttributionBuilder(mapboxMap).build();
|
||||
showAttributionDialog();
|
||||
}
|
||||
|
||||
private void showAttributionDialog() {
|
||||
attributionKeys = attributionMap.keySet().toArray(new String[attributionMap.size()]);
|
||||
attributionTitles = getAttributionTitles();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.mapbox_attributionsDialogTitle);
|
||||
builder.setAdapter(new ArrayAdapter<>(context, R.layout.mapbox_attribution_list_item, attributionKeys), this);
|
||||
builder.setAdapter(new ArrayAdapter<>(context, R.layout.mapbox_attribution_list_item, attributionTitles), this);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private String[] getAttributionTitles() {
|
||||
List<String> titles = new ArrayList<>();
|
||||
for (Attribution attribution : attributionSet) {
|
||||
titles.add(attribution.getTitle());
|
||||
}
|
||||
return titles.toArray(new String[titles.size()]);
|
||||
}
|
||||
|
||||
// Called when someone selects an attribution or telemetry settings from the dialog
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
@@ -73,7 +79,7 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface.
|
||||
}
|
||||
|
||||
private boolean isLatestEntry(int attributionKeyIndex) {
|
||||
return attributionKeyIndex == attributionKeys.length - 1;
|
||||
return attributionKeyIndex == attributionTitles.length - 1;
|
||||
}
|
||||
|
||||
private void showTelemetryDialog() {
|
||||
@@ -105,7 +111,8 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface.
|
||||
}
|
||||
|
||||
private void showMapFeedbackWebPage(int which) {
|
||||
String url = attributionMap.get(attributionKeys[which]);
|
||||
Attribution[] attributions = attributionSet.toArray(new Attribution[attributionSet.size()]);
|
||||
String url = attributions[which].getUrl();
|
||||
if (url.contains(MAP_FEEDBACK_URL)) {
|
||||
url = buildMapFeedbackMapUrl(mapboxMap.getCameraPosition());
|
||||
}
|
||||
@@ -132,46 +139,24 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface.
|
||||
|
||||
private static class AttributionBuilder {
|
||||
|
||||
private final HashMap<String, String> map = new LinkedHashMap<>();
|
||||
private final Context context;
|
||||
private final MapboxMap mapboxMap;
|
||||
|
||||
AttributionBuilder(Context context, MapboxMap mapboxMap) {
|
||||
this.context = context.getApplicationContext();
|
||||
AttributionBuilder(MapboxMap mapboxMap) {
|
||||
this.mapboxMap = mapboxMap;
|
||||
}
|
||||
|
||||
private HashMap<String, String> build() {
|
||||
private Set<Attribution> build() {
|
||||
List<String> attributions = new ArrayList<>();
|
||||
for (Source source : mapboxMap.getSources()) {
|
||||
parseAttribution(source.getAttribution());
|
||||
attributions.add(source.getAttribution());
|
||||
}
|
||||
addTelemetryEntryToAttributionMap();
|
||||
return map;
|
||||
}
|
||||
|
||||
private void parseAttribution(String attributionSource) {
|
||||
if (!TextUtils.isEmpty(attributionSource)) {
|
||||
SpannableStringBuilder htmlBuilder = (SpannableStringBuilder) Html.fromHtml(attributionSource);
|
||||
URLSpan[] urlSpans = htmlBuilder.getSpans(0, htmlBuilder.length(), URLSpan.class);
|
||||
for (URLSpan urlSpan : urlSpans) {
|
||||
map.put(resolveAnchorValue(htmlBuilder, urlSpan), urlSpan.getURL());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveAnchorValue(SpannableStringBuilder htmlBuilder, URLSpan urlSpan) {
|
||||
int start = htmlBuilder.getSpanStart(urlSpan);
|
||||
int end = htmlBuilder.getSpanEnd(urlSpan);
|
||||
int length = end - start;
|
||||
char[] charKey = new char[length];
|
||||
htmlBuilder.getChars(start, end, charKey, 0);
|
||||
return String.valueOf(charKey);
|
||||
}
|
||||
|
||||
private void addTelemetryEntryToAttributionMap() {
|
||||
String telemetryKey = context.getString(R.string.mapbox_telemetrySettings);
|
||||
String telemetryLink = context.getString(R.string.mapbox_telemetryLink);
|
||||
map.put(telemetryKey, telemetryLink);
|
||||
return new AttributionParser.Options()
|
||||
.withCopyrightSign(true)
|
||||
.withImproveMap(true)
|
||||
.withTelemetryAttribution(true)
|
||||
.withAttributionData(attributions.toArray(new String[attributions.size()]))
|
||||
.build().getAttributions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MapSnapshot {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the bitmap
|
||||
* @return the large
|
||||
*/
|
||||
public Bitmap getBitmap() {
|
||||
return bitmap;
|
||||
|
||||
@@ -5,19 +5,31 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.text.Html;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mapbox.mapboxsdk.R;
|
||||
import com.mapbox.mapboxsdk.attribution.AttributionLayout;
|
||||
import com.mapbox.mapboxsdk.attribution.AttributionMeasure;
|
||||
import com.mapbox.mapboxsdk.attribution.AttributionParser;
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||
import com.mapbox.mapboxsdk.constants.Style;
|
||||
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
|
||||
import com.mapbox.mapboxsdk.storage.FileSource;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* The map snapshotter creates a bitmap of the map, rendered
|
||||
* The map snapshotter creates a large of the map, rendered
|
||||
* off the UI thread. The snapshotter itself must be used on
|
||||
* the UI thread (for access to the main looper)
|
||||
*/
|
||||
@@ -269,43 +281,126 @@ public class MapSnapshotter {
|
||||
* @param mapSnapshot the map snapshot to draw the overlay on
|
||||
*/
|
||||
protected void addOverlay(MapSnapshot mapSnapshot) {
|
||||
Bitmap original = mapSnapshot.getBitmap();
|
||||
Canvas canvas = new Canvas(original);
|
||||
addLogo(canvas, original);
|
||||
Bitmap snapshot = mapSnapshot.getBitmap();
|
||||
Canvas canvas = new Canvas(snapshot);
|
||||
int margin = (int) context.getResources().getDisplayMetrics().density * LOGO_MARGIN_DP;
|
||||
drawOverlay(mapSnapshot, snapshot, canvas, margin);
|
||||
}
|
||||
|
||||
private void drawOverlay(MapSnapshot mapSnapshot, Bitmap snapshot, Canvas canvas, int margin) {
|
||||
AttributionMeasure measure = getAttributionMeasure(mapSnapshot, snapshot, margin);
|
||||
AttributionLayout layout = measure.measure();
|
||||
drawLogo(mapSnapshot, canvas, margin, layout);
|
||||
drawAttribution(mapSnapshot, canvas, measure, layout);
|
||||
}
|
||||
|
||||
private AttributionMeasure getAttributionMeasure(MapSnapshot mapSnapshot, Bitmap snapshot, int margin) {
|
||||
Logo logo = createScaledLogo(snapshot);
|
||||
TextView longText = createTextView(mapSnapshot, false, logo.getScale());
|
||||
TextView shortText = createTextView(mapSnapshot, true, logo.getScale());
|
||||
|
||||
return new AttributionMeasure.Builder()
|
||||
.setSnapshot(snapshot)
|
||||
.setLogo(logo.getLarge())
|
||||
.setLogoSmall(logo.getSmall())
|
||||
.setTextView(longText)
|
||||
.setTextViewShort(shortText)
|
||||
.setMarginPadding(margin)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void drawLogo(MapSnapshot mapSnapshot, Canvas canvas, int margin, AttributionLayout layout) {
|
||||
if (mapSnapshot.isShowLogo()) {
|
||||
drawLogo(mapSnapshot.getBitmap(), canvas, margin, layout);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLogo(Bitmap snapshot, Canvas canvas, int margin, AttributionLayout placement) {
|
||||
Bitmap selectedLogo = placement.getLogo();
|
||||
if (selectedLogo != null) {
|
||||
canvas.drawBitmap(selectedLogo, margin, snapshot.getHeight() - selectedLogo.getHeight() - margin, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAttribution(MapSnapshot mapSnapshot, Canvas canvas,
|
||||
AttributionMeasure measure, AttributionLayout layout) {
|
||||
// draw attribution
|
||||
PointF anchorPoint = layout.getAnchorPoint();
|
||||
if (anchorPoint != null) {
|
||||
drawAttribution(canvas, measure, anchorPoint);
|
||||
} else {
|
||||
Bitmap snapshot = mapSnapshot.getBitmap();
|
||||
Timber.e("Could not generate attribution for snapshot size: %s x %s."
|
||||
+ " You are required to provide your own attribution for the used sources: %s",
|
||||
snapshot.getWidth(), snapshot.getHeight(), mapSnapshot.getAttributions());
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAttribution(Canvas canvas, AttributionMeasure measure, PointF anchorPoint) {
|
||||
canvas.save();
|
||||
canvas.translate(anchorPoint.x, anchorPoint.y);
|
||||
measure.getTextView().draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private TextView createTextView(MapSnapshot mapSnapshot, boolean shortText, float scale) {
|
||||
int textColor = ResourcesCompat.getColor(context.getResources(), R.color.mapbox_gray_dark, context.getTheme());
|
||||
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
TextView textView = new TextView(context);
|
||||
textView.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
);
|
||||
textView.setSingleLine(true);
|
||||
textView.setTextSize(10 * scale);
|
||||
textView.setTextColor(textColor);
|
||||
textView.setBackgroundResource(R.drawable.mapbox_rounded_corner);
|
||||
textView.setText(Html.fromHtml(createAttributionString(mapSnapshot, shortText)));
|
||||
textView.measure(widthMeasureSpec, heightMeasureSpec);
|
||||
textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight());
|
||||
return textView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a logo on the canvas created from the map snapshot.
|
||||
* Create the attribution string.
|
||||
*
|
||||
* @param canvas the canvas to draw the bitmap on
|
||||
* @param original the map snapshot image
|
||||
* @param mapSnapshot the map snapshot to create the attribution for
|
||||
* @param shortText indicates if the short variant of the string should be parsed
|
||||
* @return the parsed attribution string
|
||||
*/
|
||||
private void addLogo(Canvas canvas, Bitmap original) {
|
||||
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
|
||||
float margin = displayMetrics.density * LOGO_MARGIN_DP;
|
||||
Bitmap logo = createScaledLogo(original);
|
||||
canvas.drawBitmap(logo, margin, original.getHeight() - logo.getHeight() - margin, null);
|
||||
private String createAttributionString(MapSnapshot mapSnapshot, boolean shortText) {
|
||||
AttributionParser attributionParser = new AttributionParser.Options()
|
||||
.withAttributionData(mapSnapshot.getAttributions())
|
||||
.withCopyrightSign(false)
|
||||
.withImproveMap(false)
|
||||
.build();
|
||||
|
||||
return attributionParser.createAttributionString(shortText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scaled logo for a map snapshot.
|
||||
*
|
||||
* @param snapshot the map snapshot where the logo should be placed on
|
||||
* @return the scaled bitmap logo
|
||||
* @return the scaled large logo
|
||||
*/
|
||||
private Bitmap createScaledLogo(Bitmap snapshot) {
|
||||
private Logo createScaledLogo(@NonNull Bitmap snapshot) {
|
||||
Bitmap logo = BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_icon, null);
|
||||
float scale = calculateLogoScale(snapshot, logo);
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postScale(scale, scale);
|
||||
return Bitmap.createBitmap(logo, 0, 0, logo.getWidth(), logo.getHeight(), matrix, true);
|
||||
Bitmap helmet = BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_helmet, null);
|
||||
Bitmap large = Bitmap.createBitmap(logo, 0, 0, logo.getWidth(), logo.getHeight(), matrix, true);
|
||||
Bitmap small = Bitmap.createBitmap(helmet, 0, 0, helmet.getWidth(), helmet.getHeight(), matrix, true);
|
||||
return new Logo(large, small, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scale of the logo, only allow downscaling.
|
||||
*
|
||||
* @param snapshot the bitmap of the map snapshot
|
||||
* @param logo the bitmap of the mapbox logo
|
||||
* @param snapshot the large of the map snapshot
|
||||
* @param logo the large of the mapbox logo
|
||||
* @return the scale value
|
||||
*/
|
||||
private float calculateLogoScale(Bitmap snapshot, Bitmap logo) {
|
||||
@@ -315,7 +410,14 @@ public class MapSnapshotter {
|
||||
float prefWidth = logo.getWidth() / widthRatio;
|
||||
float prefHeight = logo.getHeight() / heightRatio;
|
||||
float calculatedScale = Math.min(prefWidth / logo.getWidth(), prefHeight / logo.getHeight()) * 2;
|
||||
return calculatedScale < 1 ? calculatedScale : 1.0f;
|
||||
if (calculatedScale > 1) {
|
||||
// don't allow over-scaling
|
||||
calculatedScale = 1.0f;
|
||||
} else if (calculatedScale < 0.60f) {
|
||||
// don't scale to low either
|
||||
calculatedScale = 0.60f;
|
||||
}
|
||||
return calculatedScale;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,14 +426,17 @@ public class MapSnapshotter {
|
||||
*
|
||||
* @param snapshot the generated snapshot
|
||||
*/
|
||||
protected void onSnapshotReady(MapSnapshot snapshot) {
|
||||
if (callback != null) {
|
||||
if (snapshot.isShowLogo()) {
|
||||
addOverlay(snapshot);
|
||||
protected void onSnapshotReady(final MapSnapshot snapshot) {
|
||||
new Handler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (callback != null) {
|
||||
addOverlay(snapshot);
|
||||
callback.onSnapshotReady(snapshot);
|
||||
reset();
|
||||
}
|
||||
}
|
||||
callback.onSnapshotReady(snapshot);
|
||||
reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,4 +469,28 @@ public class MapSnapshotter {
|
||||
|
||||
@Override
|
||||
protected native void finalize() throws Throwable;
|
||||
|
||||
private class Logo {
|
||||
private Bitmap large;
|
||||
private Bitmap small;
|
||||
private float scale;
|
||||
|
||||
public Logo(Bitmap large, Bitmap small, float scale) {
|
||||
this.large = large;
|
||||
this.small = small;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public Bitmap getLarge() {
|
||||
return large;
|
||||
}
|
||||
|
||||
public Bitmap getSmall() {
|
||||
return small;
|
||||
}
|
||||
|
||||
public float getScale() {
|
||||
return scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user