diff --git a/app/build.gradle b/app/build.gradle index ec30d13d1..28c995e48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,6 +113,7 @@ dependencies { implementation project(':plugin-annotation') implementation project(':plugin-markerview') implementation project(':ktx-mapbox-maps') + implementation project(':plugin-scalebar') } apply from: "${rootDir}/gradle/checkstyle.gradle" diff --git a/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/scalebar/ScaleBarTest.java b/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/scalebar/ScaleBarTest.java new file mode 100644 index 000000000..17464e5e6 --- /dev/null +++ b/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/scalebar/ScaleBarTest.java @@ -0,0 +1,250 @@ +package com.mapbox.mapboxsdk.plugins.scalebar; + + +import android.app.Activity; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.content.ContextCompat; +import android.view.View; + +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.plugins.BaseActivityTest; +import com.mapbox.mapboxsdk.plugins.testapp.R; +import com.mapbox.mapboxsdk.plugins.testapp.activity.TestActivity; +import com.mapbox.pluginscalebar.ScaleBarOptions; +import com.mapbox.pluginscalebar.ScaleBarPlugin; +import com.mapbox.pluginscalebar.ScaleBarWidget; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import timber.log.Timber; + +import static com.mapbox.mapboxsdk.plugins.annotation.MapboxMapAction.invoke; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Basic smoke tests for ScaleBar + */ +@RunWith(AndroidJUnit4.class) +public class ScaleBarTest extends BaseActivityTest { + private ScaleBarPlugin scaleBarPlugin; + private Activity activity; + private ScaleBarWidget scaleBarWidget; + + @Override + protected Class getActivityClass() { + return TestActivity.class; + } + + private void setupScaleBar() { + Timber.i("Retrieving layer"); + invoke(mapboxMap, (uiController, mapboxMap) -> { + scaleBarPlugin = new ScaleBarPlugin(idlingResource.getMapView(), mapboxMap); + activity = rule.getActivity(); + scaleBarWidget = scaleBarPlugin.create(new ScaleBarOptions(activity)); + assertNotNull(scaleBarPlugin); + assertNotNull(scaleBarWidget); + }); + } + + @Test + public void testScaleBarEnable() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(View.VISIBLE, scaleBarWidget.getVisibility()); + assertTrue(scaleBarPlugin.isEnabled()); + scaleBarPlugin.setEnabled(false); + assertEquals(View.GONE, scaleBarWidget.getVisibility()); + assertFalse(scaleBarPlugin.isEnabled()); + }); + } + + @Test + public void testScaleBarColor() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(ContextCompat.getColor(activity, android.R.color.black), scaleBarWidget.getTextColor()); + assertEquals(ContextCompat.getColor(activity, android.R.color.black), scaleBarWidget.getPrimaryColor()); + assertEquals(ContextCompat.getColor(activity, android.R.color.white), scaleBarWidget.getSecondaryColor()); + + + int textColor = R.color.colorAccent; + int colorPrimary = R.color.colorPrimary; + int colorSecondary = R.color.colorPrimaryDark; + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setTextColor(textColor); + option.setPrimaryColor(colorPrimary); + option.setSecondaryColor(colorSecondary); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(ContextCompat.getColor(activity, textColor), scaleBarWidget.getTextColor()); + assertEquals(ContextCompat.getColor(activity, colorPrimary), scaleBarWidget.getPrimaryColor()); + assertEquals(ContextCompat.getColor(activity, colorSecondary), scaleBarWidget.getSecondaryColor()); + }); + } + + @Test + public void testScaleBarWidth() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(MapView.class, scaleBarWidget.getParent().getClass()); + MapView parent = (MapView) scaleBarWidget.getParent(); + assertEquals(parent.getWidth(), scaleBarWidget.getMapViewWidth()); + }); + } + + @Test + public void testMargin() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_margin_left), + scaleBarWidget.getMarginLeft(), 0); + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_margin_top), + scaleBarWidget.getMarginTop(), 0); + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_text_margin), + scaleBarWidget.getTextBarMargin(), 0); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setMarginLeft(R.dimen.fab_margin); + option.setMarginTop(R.dimen.fab_margin); + option.setTextBarMargin(R.dimen.fab_margin); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getMarginLeft(), 0); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getMarginTop(), 0); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getTextBarMargin(), 0); + + option = new ScaleBarOptions(activity); + option.setMarginLeft(100f); + option.setMarginTop(50f); + option.setTextBarMargin(30f); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(100f, scaleBarWidget.getMarginLeft(), 0); + assertEquals(50f, scaleBarWidget.getMarginTop(), 0); + assertEquals(30f, scaleBarWidget.getTextBarMargin(), 0); + + }); + } + + @Test + public void testBarHeight() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_height), + scaleBarWidget.getBarHeight(), 0); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setBarHeight(R.dimen.fab_margin); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getBarHeight(), 0); + + option = new ScaleBarOptions(activity); + option.setBarHeight(100f); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(100f, scaleBarWidget.getBarHeight(), 0); + + }); + } + + @Test + public void testTextSize() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_text_size), + scaleBarWidget.getTextSize(), 0); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setTextSize(R.dimen.fab_margin); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getTextSize(), 0); + + option = new ScaleBarOptions(activity); + option.setTextSize(100f); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(100f, scaleBarWidget.getTextSize(), 0); + + }); + } + + @Test + public void testBorderWidth() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(activity.getResources().getDimension(R.dimen.mapbox_scale_bar_border_width), + scaleBarWidget.getBorderWidth(), 0); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setBorderWidth(R.dimen.fab_margin); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(activity.getResources().getDimension(R.dimen.fab_margin), + scaleBarWidget.getBorderWidth(), 0); + + option = new ScaleBarOptions(activity); + option.setBorderWidth(100f); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(100f, scaleBarWidget.getBorderWidth(), 0); + + }); + } + + + @Test + public void testRefreshInterval() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(ScaleBarOptions.REFRESH_INTERVAL_DEFAULT, scaleBarWidget.getRefreshInterval(), 0); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setRefreshInterval(1000); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertEquals(1000, scaleBarWidget.getRefreshInterval(), 0); + + }); + } + + @Test + public void testMetrics() { + validateTestSetup(); + setupScaleBar(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + assertEquals(ScaleBarOptions.LocaleUnitResolver.isMetricSystem(), scaleBarWidget.isMetricUnit()); + + ScaleBarOptions option = new ScaleBarOptions(activity); + option.setMetricUnit(true); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertTrue(scaleBarWidget.isMetricUnit()); + + option = new ScaleBarOptions(activity); + option.setMetricUnit(false); + scaleBarWidget = scaleBarPlugin.create(option); + assertNotNull(scaleBarWidget); + assertFalse(scaleBarWidget.isMetricUnit()); + }); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2b9f9947..4b893a088 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -249,6 +249,18 @@ android:value=".activity.FeatureOverviewActivity" /> + + + + + diff --git a/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/scalebar/ScalebarActivity.kt b/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/scalebar/ScalebarActivity.kt new file mode 100644 index 000000000..abf08b374 --- /dev/null +++ b/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/scalebar/ScalebarActivity.kt @@ -0,0 +1,82 @@ +package com.mapbox.mapboxsdk.plugins.testapp.activity.scalebar + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.testapp.R +import com.mapbox.pluginscalebar.ScaleBarOptions +import com.mapbox.pluginscalebar.ScaleBarPlugin +import kotlinx.android.synthetic.main.activity_scalebar.* + +/** + * Activity showing a scalebar used on a MapView. + */ +class ScalebarActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_scalebar) + mapView.onCreate(savedInstanceState) + mapView.getMapAsync { mapboxMap -> + mapboxMap.setStyle(Style.MAPBOX_STREETS) { + addScalebar(mapboxMap) + } + } + } + + private fun addScalebar(mapboxMap: MapboxMap) { + val scaleBarPlugin = ScaleBarPlugin(mapView, mapboxMap) + val scaleBarOptions = ScaleBarOptions(this) + scaleBarOptions + .setTextColor(R.color.mapboxRed) + .setTextSize(20f) + .setBarHeight(15f) + .setBorderWidth(5f) + .setMetricUnit(true) + .setRefreshInterval(15) + .setMarginTop(15f) + .setMarginLeft(16f) + .setTextBarMargin(15f) + + scaleBarPlugin.create(scaleBarOptions) + fabScaleWidget.setOnClickListener { + scaleBarPlugin.isEnabled = !scaleBarPlugin.isEnabled + } + } + + + override fun onStart() { + super.onStart() + mapView.onStart() + } + + override fun onResume() { + super.onResume() + mapView.onResume() + } + + override fun onPause() { + super.onPause() + mapView.onPause() + } + + override fun onStop() { + super.onStop() + mapView.onStop() + } + + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } +} diff --git a/app/src/main/res/drawable/ic_scale.xml b/app/src/main/res/drawable/ic_scale.xml new file mode 100644 index 000000000..0de4c28f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_scale.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scalebar.xml b/app/src/main/res/layout/activity_scalebar.xml new file mode 100644 index 000000000..00ab4be91 --- /dev/null +++ b/app/src/main/res/layout/activity_scalebar.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72f16c108..fd05fc15b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Annotation plugin Kotlin extension functions View synchronisation + Scalebar Traffic Plugin @@ -34,6 +35,7 @@ Change Line MarkerView plugin Mapbox Maps KTX + Scale bar Add Traffic layers to any Mapbox basemap. @@ -56,6 +58,7 @@ Show circles on a map Synchronise a view on a map Test Mapbox Maps SDK for Android Kotlin extension functions + Show scale bar on a map Example shows how to launch the Place Picker using the Floating action button and receiving a result in onActivityResult. diff --git a/plugin-scalebar/.gitignore b/plugin-scalebar/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/plugin-scalebar/.gitignore @@ -0,0 +1 @@ +/build diff --git a/plugin-scalebar/build.gradle b/plugin-scalebar/build.gradle new file mode 100644 index 000000000..a28c8586e --- /dev/null +++ b/plugin-scalebar/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion androidVersions.compileSdkVersion + + defaultConfig { + minSdkVersion androidVersions.minSdkVersion + targetSdkVersion androidVersions.targetSdkVersion + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + configurations { + javadocDeps + } + + lintOptions { + abortOnError false + } + + testOptions { + unitTests.returnDefaultValues true + } +} + +dependencies { + implementation dependenciesList.mapboxMapSdk + javadocDeps dependenciesList.mapboxMapSdk + testImplementation dependenciesList.junit + testImplementation dependenciesList.mockito +} + +apply from: "${rootDir}/gradle/javadoc.gradle" +apply from: "${rootDir}/gradle/publish.gradle" +apply from: "${rootDir}/gradle/checkstyle.gradle" \ No newline at end of file diff --git a/plugin-scalebar/gradle.properties b/plugin-scalebar/gradle.properties new file mode 100644 index 000000000..e411d8081 --- /dev/null +++ b/plugin-scalebar/gradle.properties @@ -0,0 +1,5 @@ +VERSION_NAME=0.1.0-SNAPSHOT +POM_ARTIFACT_ID=mapbox-android-plugin-scalebar +POM_NAME=Mapbox Android Scalebar Plugin +POM_DESCRIPTION=Mapbox Android Scalebar Plugin +POM_PACKAGING=aar \ No newline at end of file diff --git a/plugin-scalebar/proguard-rules.pro b/plugin-scalebar/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/plugin-scalebar/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/plugin-scalebar/src/main/AndroidManifest.xml b/plugin-scalebar/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c2d2d0e94 --- /dev/null +++ b/plugin-scalebar/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarConstants.java b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarConstants.java new file mode 100644 index 000000000..8a170f4f1 --- /dev/null +++ b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarConstants.java @@ -0,0 +1,79 @@ +package com.mapbox.pluginscalebar; + +import android.util.Pair; + +import java.util.ArrayList; + +class ScaleBarConstants { + static double FEET_PER_METER = 3.2808; + static int FEET_PER_MILE = 5280; + static String METER_UNIT = " m"; + static String FEET_UNIT = " ft"; + static ArrayList> metricTable = new ArrayList>() { + { + add(new Pair<>(1, 2)); + add(new Pair<>(2, 2)); + add(new Pair<>(4, 2)); + add(new Pair<>(10, 2)); + add(new Pair<>(20, 2)); + add(new Pair<>(50, 2)); + add(new Pair<>(75, 3)); + add(new Pair<>(100, 2)); + add(new Pair<>(150, 2)); + add(new Pair<>(200, 2)); + add(new Pair<>(300, 3)); + add(new Pair<>(500, 2)); + add(new Pair<>(1000, 2)); + add(new Pair<>(1500, 2)); + add(new Pair<>(3000, 3)); + add(new Pair<>(5000, 2)); + add(new Pair<>(10000, 2)); + add(new Pair<>(20000, 2)); + add(new Pair<>(30000, 3)); + add(new Pair<>(50000, 2)); + add(new Pair<>(100000, 2)); + add(new Pair<>(200000, 2)); + add(new Pair<>(300000, 3)); + add(new Pair<>(400000, 2)); + add(new Pair<>(500000, 2)); + add(new Pair<>(600000, 3)); + add(new Pair<>(800000, 2)); + } + }; + + static ArrayList> imperialTable = new ArrayList>() { + { + add(new Pair<>(4, 2)); + add(new Pair<>(6, 2)); + add(new Pair<>(10, 2)); + add(new Pair<>(20, 2)); + add(new Pair<>(30, 2)); + add(new Pair<>(50, 2)); + add(new Pair<>(75, 3)); + add(new Pair<>(100, 2)); + add(new Pair<>(200, 2)); + add(new Pair<>(300, 3)); + add(new Pair<>(400, 2)); + add(new Pair<>(600, 3)); + add(new Pair<>(800, 2)); + add(new Pair<>(1000, 2)); + add(new Pair<>((int) (0.25f * FEET_PER_MILE), 2)); + add(new Pair<>((int) (0.5f * FEET_PER_MILE), 2)); + add(new Pair<>(FEET_PER_MILE, 2)); + add(new Pair<>(2 * FEET_PER_MILE, 2)); + add(new Pair<>(3 * FEET_PER_MILE, 3)); + add(new Pair<>(4 * FEET_PER_MILE, 2)); + add(new Pair<>(8 * FEET_PER_MILE, 2)); + add(new Pair<>(12 * FEET_PER_MILE, 2)); + add(new Pair<>(15 * FEET_PER_MILE, 3)); + add(new Pair<>(20 * FEET_PER_MILE, 2)); + add(new Pair<>(30 * FEET_PER_MILE, 3)); + add(new Pair<>(40 * FEET_PER_MILE, 2)); + add(new Pair<>(80 * FEET_PER_MILE, 2)); + add(new Pair<>(120 * FEET_PER_MILE, 2)); + add(new Pair<>(200 * FEET_PER_MILE, 2)); + add(new Pair<>(300 * FEET_PER_MILE, 3)); + add(new Pair<>(400 * FEET_PER_MILE, 2)); + } + }; +} diff --git a/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarOptions.java b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarOptions.java new file mode 100644 index 000000000..c4a70c9a2 --- /dev/null +++ b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarOptions.java @@ -0,0 +1,297 @@ +package com.mapbox.pluginscalebar; + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; + +import java.util.Locale; + +/** + * Builder class from which a scale bar is created. + */ +public class ScaleBarOptions { + public static final int REFRESH_INTERVAL_DEFAULT = 15; + + private final Context context; + private int refreshInterval; + private int textColor; + private int primaryColor; + private int secondaryColor; + private float marginTop; + private float marginLeft; + private float textBarMargin; + private float barHeight; + private float borderWidth; + private float textSize; + private boolean isMetricUnit; + + public ScaleBarOptions(@NonNull Context context) { + this.context = context; + refreshInterval = REFRESH_INTERVAL_DEFAULT; + setBarHeight(R.dimen.mapbox_scale_bar_height); + setBorderWidth(R.dimen.mapbox_scale_bar_border_width); + setTextSize(R.dimen.mapbox_scale_bar_text_size); + setMarginTop(R.dimen.mapbox_scale_bar_margin_top); + setMarginLeft(R.dimen.mapbox_scale_bar_margin_left); + setTextBarMargin(R.dimen.mapbox_scale_bar_text_margin); + isMetricUnit = LocaleUnitResolver.isMetricSystem(); + setTextColor(android.R.color.black); + setPrimaryColor(android.R.color.black); + setSecondaryColor(android.R.color.white); + } + + /** + * Build a scale bar widget instance with current option settings. + * + * @return The built ScaleBarWidget instance. + */ + ScaleBarWidget build() { + ScaleBarWidget scaleBarWidget = new ScaleBarWidget(context); + scaleBarWidget.setBarHeight(barHeight); + scaleBarWidget.setBorderWidth(borderWidth); + scaleBarWidget.setMarginLeft(marginLeft); + scaleBarWidget.setMarginTop(marginTop); + scaleBarWidget.setTextBarMargin(textBarMargin); + scaleBarWidget.setMetricUnit(isMetricUnit); + scaleBarWidget.setRefreshInterval(refreshInterval); + scaleBarWidget.setPrimaryColor(primaryColor); + scaleBarWidget.setSecondaryColor(secondaryColor); + scaleBarWidget.setTextColor(textColor); + scaleBarWidget.setTextSize(textSize); + return scaleBarWidget; + } + + /** + * Set plugin's minimum refresh interval, + * default value is {@link #REFRESH_INTERVAL_DEFAULT}. + * + * @param refreshInterval the min refresh interval, in millisecond. + * @return this + */ + public ScaleBarOptions setRefreshInterval(int refreshInterval) { + this.refreshInterval = refreshInterval; + return this; + } + + /** + * Set the text color on scale bar, + * default value is android.R.color.black. + * + * @param textColor the text color on scale bar. + * @return this. + */ + public ScaleBarOptions setTextColor(@ColorRes int textColor) { + this.textColor = ContextCompat.getColor(context, textColor); + return this; + } + + /** + * Set the primary color of the scale bar, will be used to draw odd index blocks, + * default value is android.R.color.black. + * + * @param primaryColor the primary color of the scale bar. + * @return this. + */ + public ScaleBarOptions setPrimaryColor(@ColorRes int primaryColor) { + this.primaryColor = ContextCompat.getColor(context, primaryColor); + return this; + } + + /** + * Set the secondary color of the scale bar, will be used to draw even index blocks, + * default value is android.R.color.white. + * + * @param secondaryColor the secondaryColor color of the scale bar. + * @return this. + */ + public ScaleBarOptions setSecondaryColor(@ColorRes int secondaryColor) { + this.secondaryColor = ContextCompat.getColor(context, secondaryColor); + return this; + } + + /** + * Set the margin between scale bar and the top of mapView. + * + * @param marginTop the margin between scale bar and the top of mapView, in pixel. + * @return this. + */ + public ScaleBarOptions setMarginTop(float marginTop) { + this.marginTop = marginTop; + return this; + } + + /** + * Set the margin between scale bar and the top of mapView. + * + * @param marginTop the margin between scale bar and the top of mapView, in dp. + * @return this. + */ + public ScaleBarOptions setMarginTop(@DimenRes int marginTop) { + this.marginTop = context.getResources().getDimension(marginTop); + return this; + } + + /** + * Set the height for blocks in scale bar. + * + * @param barHeight the height for blocks in scale bar, in pixel. + * @return this. + */ + public ScaleBarOptions setBarHeight(float barHeight) { + this.barHeight = barHeight; + return this; + } + + /** + * Set the height for blocks in scale bar. + * + * @param barHeight the height for blocks in scale bar, in dp. + * @return this. + */ + public ScaleBarOptions setBarHeight(@DimenRes int barHeight) { + this.barHeight = context.getResources().getDimension(barHeight); + return this; + } + + /** + * Set the border width in scale bar. + * + * @param borderWidth the border width in scale bar, in pixel. + * @return this. + */ + public ScaleBarOptions setBorderWidth(float borderWidth) { + this.borderWidth = borderWidth; + return this; + } + + /** + * Set the border width in scale bar. + * + * @param borderWidth the border width in scale bar, in dp. + * @return this. + */ + public ScaleBarOptions setBorderWidth(@DimenRes int borderWidth) { + this.borderWidth = context.getResources().getDimension(borderWidth); + return this; + } + + /** + * Set the text size of scale bar. + * + * @param textSize the text size of scale bar, in pixel. + * @return this. + */ + public ScaleBarOptions setTextSize(float textSize) { + this.textSize = textSize; + return this; + } + + /** + * Set the text size of scale bar. + * + * @param textSize the text size of scale bar, in dp. + * @return this. + */ + public ScaleBarOptions setTextSize(@DimenRes int textSize) { + this.textSize = context.getResources().getDimension(textSize); + return this; + } + + /** + * Set whether to use metric unit or not. The default value is depends on settings of device. + * + * @param metricUnit whether to use metric unit or not. + * @return this. + */ + public ScaleBarOptions setMetricUnit(boolean metricUnit) { + isMetricUnit = metricUnit; + return this; + } + + /** + * Set the left margin between scale bar and mapView. + * + * @param marginLeft the left margin between scale bar and mapView, in pixel. + * @return this. + */ + public ScaleBarOptions setMarginLeft(float marginLeft) { + this.marginLeft = marginLeft; + return this; + } + + /** + * Set the left margin between scale bar and mapView. + * + * @param marginLeft the left margin between scale bar and mapView, in dp. + * @return this. + */ + public ScaleBarOptions setMarginLeft(@DimenRes int marginLeft) { + this.marginLeft = context.getResources().getDimension(marginLeft); + return this; + } + + /** + * Set the margin between text and blocks inside scale bar. + * + * @param textBarMargin the margin between text and blocks inside scale bar, in pixel. + * @return this. + */ + public ScaleBarOptions setTextBarMargin(float textBarMargin) { + this.textBarMargin = textBarMargin; + return this; + } + + /** + * Set the margin between text and blocks inside scale bar. + * + * @param textBarMargin the margin between text and blocks inside scale bar, in dp. + * @return this. + */ + public ScaleBarOptions setTextBarMargin(@DimenRes int textBarMargin) { + this.textBarMargin = context.getResources().getDimension(textBarMargin); + return this; + } + + /** + * Helper class to determine the user measuring system. + *

+ * Currently supports differentiating between metric vs imperial system. + *

+ *

+ * Depends on {@link Locale#getDefault()} which returns the locale used at application startup. + *

+ */ + @VisibleForTesting + public static class LocaleUnitResolver { + + /** + * Returns true if the user is in a country using the metric system. + * + * @return true if user country is using metric, false if imperial + */ + public static boolean isMetricSystem() { + String countryCode = Locale.getDefault().getCountry().toUpperCase(); + switch (countryCode) { + case ImperialCountryCode.US: + case ImperialCountryCode.LIBERIA: + case ImperialCountryCode.MYANMAR: + return false; + default: + return true; + } + } + + /** + * Data class containing uppercase country codes for countries using the imperial system. + */ + static class ImperialCountryCode { + static final String US = "US"; + static final String MYANMAR = "MM"; + static final String LIBERIA = "LR"; + } + } + +} diff --git a/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarPlugin.java b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarPlugin.java new file mode 100644 index 000000000..3af4ff3a0 --- /dev/null +++ b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarPlugin.java @@ -0,0 +1,92 @@ +package com.mapbox.pluginscalebar; + +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.view.View; + +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; + +/** + * Plugin class that shows a scale bar on MapView and changes the scale corresponding to the MapView's scale. + */ +public class ScaleBarPlugin { + private final MapView mapView; + private final MapboxMap mapboxMap; + private final Projection projection; + private boolean enabled = true; + private ScaleBarWidget scaleBarWidget; + + private final MapboxMap.OnCameraMoveListener cameraMoveListener = new MapboxMap.OnCameraMoveListener() { + @Override + public void onCameraMove() { + CameraPosition cameraPosition = mapboxMap.getCameraPosition(); + double metersPerPixel = projection.getMetersPerPixelAtLatitude(cameraPosition.target.getLatitude()); + scaleBarWidget.setDistancePerPixel(projection.getMetersPerPixelAtLatitude(metersPerPixel)); + } + }; + + public ScaleBarPlugin(@NonNull MapView mapView, @NonNull MapboxMap mapboxMap) { + this.mapView = mapView; + this.mapboxMap = mapboxMap; + this.projection = mapboxMap.getProjection(); + } + + /** + * Create a scale bar widget on mapView. + * + * @param option The scale bar widget options that used to build scale bar widget. + * @return The created ScaleBarWidget instance. + */ + public ScaleBarWidget create(@NonNull ScaleBarOptions option) { + if (scaleBarWidget != null) { + mapView.removeView(scaleBarWidget); + } + scaleBarWidget = option.build(); + scaleBarWidget.setMapViewWidth(mapView.getWidth()); + mapboxMap.addOnCameraMoveListener(cameraMoveListener); + mapView.addView(scaleBarWidget); + CameraPosition cameraPosition = mapboxMap.getCameraPosition(); + double metersPerPixel = projection.getMetersPerPixelAtLatitude(cameraPosition.target.getLatitude()); + scaleBarWidget.setDistancePerPixel(projection.getMetersPerPixelAtLatitude(metersPerPixel)); + return scaleBarWidget; + } + + /** + * Returns true if the scale plugin is currently enabled and visible. + * + * @return true if enabled, false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Toggles the scale plugin state. + *

+ * If the scale plugin wasn't enabled, a {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener} + * will be added to the {@link MapView} to listen to scale change events to update the state of this plugin. If the + * plugin was enabled the {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener} + * will be removed from the map. + *

+ */ + @UiThread + public void setEnabled(boolean enabled) { + if (this.enabled == enabled) { + // already in correct state + return; + } + this.enabled = enabled; + if (scaleBarWidget != null) { + scaleBarWidget.setVisibility(enabled ? View.VISIBLE : View.GONE); + } + if (enabled) { + mapboxMap.addOnCameraMoveListener(cameraMoveListener); + } else { + mapboxMap.removeOnCameraMoveListener(cameraMoveListener); + } + } + +} diff --git a/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarWidget.java b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarWidget.java new file mode 100644 index 000000000..b8d350b76 --- /dev/null +++ b/plugin-scalebar/src/main/java/com/mapbox/pluginscalebar/ScaleBarWidget.java @@ -0,0 +1,362 @@ +package com.mapbox.pluginscalebar; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.util.Pair; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * The scale widget is a visual representation of the scale bar plugin. + */ +public class ScaleBarWidget extends View { + private static int MSG_WHAT = 0; + private final Paint textPaint = new Paint(); + private final Paint barPaint = new Paint(); + private int refreshInterval; + private int textColor; + private int primaryColor; + private int secondaryColor; + private int mapViewWidth; + private float marginLeft; + private float marginTop; + private float textBarMargin; + private float maxBarWidth; + private float barHeight; + private float borderWidth; + private float textSize; + private double distancePerPixel; + private boolean isMetricUnit; + private ArrayList> scaleTable; + private String unit; + private final RefreshHandler refreshHandler; + + ScaleBarWidget(@NonNull Context context) { + super(context); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Paint.Align.CENTER); + barPaint.setAntiAlias(true); + refreshHandler = new RefreshHandler(this); + } + + @Override + protected void onDraw(Canvas canvas) { + if (distancePerPixel <= 0) { + return; + } + double maxDistance = mapViewWidth * distancePerPixel / 2; + Pair pair = scaleTable.get(0); + for (int i = 1; i < scaleTable.size(); i++) { + pair = scaleTable.get(i); + if (pair.first > maxDistance) { + //use the last scale here, otherwise the scale will be too large + pair = scaleTable.get(i - 1); + break; + } + } + + int unitDistance = pair.first / pair.second; + float unitBarWidth = maxBarWidth / 2f; + if (unitDistance == 0) { + unitDistance = 1; + } else { + unitBarWidth = (float) (unitDistance / distancePerPixel); + } + //Drawing the surrounding borders + barPaint.setStyle(Paint.Style.FILL_AND_STROKE); + barPaint.setColor(secondaryColor); + + canvas.drawRect(marginLeft - borderWidth * 2, + textBarMargin + textSize + marginTop - borderWidth * 2, + marginLeft + unitBarWidth * pair.second + borderWidth * 2, + textBarMargin + textSize + marginTop + barHeight + borderWidth * 2, + barPaint); + barPaint.setColor(primaryColor); + canvas.drawRect(marginLeft - borderWidth, + textBarMargin + textSize + marginTop - borderWidth, + marginLeft + unitBarWidth * pair.second + borderWidth, + textBarMargin + textSize + marginTop + barHeight + borderWidth, + barPaint); + + //Drawing the fill + barPaint.setStyle(Paint.Style.FILL); + int i = 0; + for (; i < pair.second; i++) { + barPaint.setColor(i % 2 == 0 ? primaryColor : secondaryColor); + String text = i == 0 ? String.valueOf(unitDistance * i) : unitDistance * i + unit; + canvas.drawText(text, + marginLeft + unitBarWidth * i, + textSize + marginTop, + textPaint); + canvas.drawRect(marginLeft + unitBarWidth * i, + textBarMargin + textSize + marginTop, + marginLeft + unitBarWidth * (1 + i), + textBarMargin + textSize + marginTop + barHeight, + barPaint); + } + canvas.drawText(unitDistance * i + unit, + marginLeft + unitBarWidth * i, + textSize + marginTop, + textPaint); + } + + /** + * Update the scale when mapView's scale has changed. + * + * @param metersPerPixel how many meters in each pixel. + */ + void setDistancePerPixel(double metersPerPixel) { + this.distancePerPixel = isMetricUnit ? metersPerPixel : metersPerPixel * ScaleBarConstants.FEET_PER_METER; + if (!refreshHandler.hasMessages(MSG_WHAT)) { + refreshHandler.sendEmptyMessageDelayed(MSG_WHAT, refreshInterval); + } + } + + /** + * Get plugin's minimum refresh interval, in millisecond. + * + * @return refresh duration + */ + public int getRefreshInterval() { + return refreshInterval; + } + + /** + * Set plugin's minimum refresh interval, in millisecond. + * + * @param refreshInterval the refresh duration. + */ + public void setRefreshInterval(int refreshInterval) { + this.refreshInterval = refreshInterval; + } + + /** + * Get the margin between text and blocks. + * + * @return margin between text and blocks, in pixel. + */ + public float getTextBarMargin() { + return textBarMargin; + } + + /** + * Set the margin between text and blocks inside scale bar. + * + * @param textBarMargin the margin between text and blocks inside scale bar, in pixel. + */ + public void setTextBarMargin(float textBarMargin) { + this.textBarMargin = textBarMargin; + } + + /** + * Get the left margin between scale bar and mapView. + * + * @return the left margin between scale bar and mapView, in pixel + */ + public float getMarginLeft() { + return marginLeft; + } + + /** + * Set the left margin between scale bar and mapView. + * + * @param marginLeft the left margin between scale bar and mapView, in pixel. + */ + public void setMarginLeft(float marginLeft) { + this.marginLeft = marginLeft; + maxBarWidth = mapViewWidth / 2f - marginLeft; + } + + /** + * Get the bar height for blocks. + * + * @return the height for blocks in scale bar, in pixel. + */ + public float getBarHeight() { + return barHeight; + } + + /** + * Set the height for blocks in scale bar. + * + * @param barHeight the height for blocks in scale bar, in pixel. + */ + public void setBarHeight(float barHeight) { + this.barHeight = barHeight; + } + + /** + * Get the margin between scale bar and the top of mapView, + * + * @return the margin between scale bar and the top of mapView, in pixel. + */ + public float getMarginTop() { + return marginTop; + } + + /** + * Set the margin between scale bar and the top of mapView。 + * + * @param marginTop the margin between scale bar and the top of mapView, in pixel. + */ + public void setMarginTop(float marginTop) { + this.marginTop = marginTop; + } + + /** + * Get the border width in scale bar. + * + * @return the border width in scale bar, in pixel + */ + public float getBorderWidth() { + return borderWidth; + } + + /** + * Set the border width in scale bar. + * + * @param borderWidth the border width in scale bar, in pixel. + */ + public void setBorderWidth(float borderWidth) { + this.borderWidth = borderWidth; + } + + /** + * Get the text size of scale bar. + * + * @return the text size of scale bar, in pixel. + */ + public float getTextSize() { + return textSize; + } + + /** + * Set the text size of scale bar. + * + * @param textSize the text size of scale bar, in pixel. + */ + public void setTextSize(float textSize) { + this.textSize = textSize; + textPaint.setTextSize(textSize); + } + + /** + * Get the current setting for metrix unit. + * + * @return true if using metrix unit, otherwise false. + */ + public boolean isMetricUnit() { + return isMetricUnit; + } + + /** + * Set whether to use metric unit or not. + * + * @param metricUnit whether to use metric unit or not. + */ + public void setMetricUnit(boolean metricUnit) { + isMetricUnit = metricUnit; + scaleTable = isMetricUnit ? ScaleBarConstants.metricTable : ScaleBarConstants.imperialTable; + unit = isMetricUnit ? ScaleBarConstants.METER_UNIT : ScaleBarConstants.FEET_UNIT; + } + + /** + * Get the color for text. + * + * @return the color for text. + */ + public int getTextColor() { + return textColor; + } + + /** + * Set the text color on scale bar, + * + * @param textColor the text color on scale bar. + */ + public void setTextColor(@ColorInt int textColor) { + this.textColor = textColor; + textPaint.setColor(textColor); + } + + /** + * Get the primary color of the scale bar. + * + * @return the primary color of the scale bar. + */ + public int getPrimaryColor() { + return primaryColor; + } + + /** + * Set the primary color of the scale bar, will be used to draw odd index blocks, + * + * @param primaryColor the primary color of the scale bar, in pixel. + */ + public void setPrimaryColor(@ColorInt int primaryColor) { + this.primaryColor = primaryColor; + } + + /** + * Get the secondary color of the scale bar. + * + * @return the secondary color of the scale bar. + */ + public int getSecondaryColor() { + return secondaryColor; + } + + /** + * Set the secondary color of the scale bar, will be used to draw even index blocks, + * + * @param secondaryColor the secondaryColor color of the scale bar, in pixel. + */ + public void setSecondaryColor(@ColorInt int secondaryColor) { + this.secondaryColor = secondaryColor; + } + + /** + * Get the width of current mapView. + * + * @return the width of current mapView. + */ + public int getMapViewWidth() { + return mapViewWidth; + } + + /** + * Set the width of current mapView. + * + * @param mapViewWidth mapView's width in pixel. + */ + void setMapViewWidth(int mapViewWidth) { + this.mapViewWidth = mapViewWidth; + maxBarWidth = mapViewWidth / 2f - marginLeft; + } + + /** + * Handler class to limit the refresh frequent. + */ + private static class RefreshHandler extends Handler { + WeakReference scaleBarWidgetWeakReference; + + RefreshHandler(ScaleBarWidget scaleBarWidget) { + scaleBarWidgetWeakReference = new WeakReference<>(scaleBarWidget); + } + + public void handleMessage(Message msg) { + ScaleBarWidget scaleBarWidget = scaleBarWidgetWeakReference.get(); + if (msg.what == MSG_WHAT && scaleBarWidget != null) { + scaleBarWidget.invalidate(); + } + } + } + +} diff --git a/plugin-scalebar/src/main/res/values/dimens.xml b/plugin-scalebar/src/main/res/values/dimens.xml new file mode 100644 index 000000000..5f56acb7c --- /dev/null +++ b/plugin-scalebar/src/main/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + 8dp + 10dp + 2dp + 8dp + 1dp + 4dp + diff --git a/settings.gradle b/settings.gradle index e29d567ac..07e316702 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,5 @@ include ':plugin-offline' include ':plugin-localization' include ':plugin-annotation' include ':plugin-markerview' -include ':ktx-mapbox-maps' \ No newline at end of file +include ':ktx-mapbox-maps' +include ':plugin-scalebar' \ No newline at end of file