diff --git a/plugins/app/build.gradle b/plugins/app/build.gradle index 7ce999c9a..c2b744c6b 100644 --- a/plugins/app/build.gradle +++ b/plugins/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion 15 targetSdkVersion 25 versionCode 1 - versionName "1.0" + versionName "0.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -20,13 +20,20 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' + compile 'com.android.support:design:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha7' testCompile 'junit:junit:4.12' + + compile 'com.jakewharton.timber:timber:4.5.1' + compile 'com.jakewharton:butterknife:8.5.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' + + compile project(':traffic') } apply from: 'gradle-checkstyle.gradle' \ No newline at end of file diff --git a/plugins/app/gradle-checkstyle.gradle b/plugins/app/gradle-checkstyle.gradle index fdc4399f1..00d55f81c 100644 --- a/plugins/app/gradle-checkstyle.gradle +++ b/plugins/app/gradle-checkstyle.gradle @@ -12,7 +12,7 @@ task checkstyle(type: Checkstyle) { source 'src' include '**/*.java' exclude '**/gen/**' - exclude '**/style/*LayerTest.java' + exclude '**/*FeatureOverviewActivity.java' classpath = files() ignoreFailures = false } diff --git a/plugins/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleInstrumentedTest.java b/plugins/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleInstrumentedTest.java deleted file mode 100644 index 05d6b30d8..000000000 --- a/plugins/app/src/androidTest/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.mapbox.mapboxsdk.plugins.testapp; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static junit.framework.Assert.assertEquals; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.mapbox.mapboxsdk.plugins.testapp", appContext.getPackageName()); - } -} diff --git a/plugins/app/src/main/AndroidManifest.xml b/plugins/app/src/main/AndroidManifest.xml index 26d06d72e..480e21f72 100644 --- a/plugins/app/src/main/AndroidManifest.xml +++ b/plugins/app/src/main/AndroidManifest.xml @@ -8,14 +8,26 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:name=".PluginApplication" android:theme="@style/AppTheme"> - + + + + + \ No newline at end of file diff --git a/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/FeatureOverviewActivity.java b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/FeatureOverviewActivity.java deleted file mode 100644 index 777e97f24..000000000 --- a/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/FeatureOverviewActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mapbox.mapboxsdk.plugins.testapp; - -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; - -public class FeatureOverviewActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_feature_overview); - } -} diff --git a/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/PluginApplication.java b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/PluginApplication.java new file mode 100644 index 000000000..008b66b38 --- /dev/null +++ b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/PluginApplication.java @@ -0,0 +1,14 @@ +package com.mapbox.mapboxsdk.plugins.testapp; + +import android.app.Application; + +import com.mapbox.mapboxsdk.Mapbox; + +public class PluginApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + Mapbox.getInstance(this, getString(R.string.mapbox_access_token)); + } +} diff --git a/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/FeatureOverviewActivity.java b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/FeatureOverviewActivity.java new file mode 100644 index 000000000..d42da8ee8 --- /dev/null +++ b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/FeatureOverviewActivity.java @@ -0,0 +1,513 @@ +package com.mapbox.mapboxsdk.plugins.testapp.activity; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.mapbox.mapboxsdk.plugins.testapp.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; + +import timber.log.Timber; + +/** + * Activity showing a RecyclerView with Activities generated from AndroidManifest.xml + */ +public class FeatureOverviewActivity extends AppCompatActivity { + + private static final String KEY_STATE_FEATURES = "featureList"; + + private RecyclerView recyclerView; + private FeatureSectionAdapter sectionAdapter; + private List features; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_feature_overview); + + recyclerView = (RecyclerView) findViewById(R.id.recyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener()); + recyclerView.setHasFixedSize(true); + + ItemClickSupport.addTo(recyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() { + @Override + public void onItemClicked(RecyclerView recyclerView, int position, View view) { + if (!sectionAdapter.isSectionHeaderPosition(position)) { + int itemPosition = sectionAdapter.getConvertedPosition(position); + Feature feature = features.get(itemPosition); + startFeature(feature); + } + } + }); + + if (savedInstanceState == null) { + loadFeatures(); + } else { + features = savedInstanceState.getParcelableArrayList(KEY_STATE_FEATURES); + onFeaturesLoaded(features); + } + } + + private void loadFeatures() { + try { + new LoadFeatureTask().execute( + getPackageManager().getPackageInfo(getPackageName(), + PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA)); + } catch (PackageManager.NameNotFoundException exception) { + Timber.e("Could not resolve package info", exception); + } + } + + private void onFeaturesLoaded(List featuresList) { + features = featuresList; + + List
sections = new ArrayList<>(); + String currentCat = ""; + for (int i = 0; i < features.size(); i++) { + String category = features.get(i).getCategory(); + if (!currentCat.equals(category)) { + sections.add(new Section(i, category)); + currentCat = category; + } + } + + Section[] dummy = new Section[sections.size()]; + sectionAdapter = new FeatureSectionAdapter( + this, R.layout.section_feature, R.id.section_text, new FeatureAdapter(features)); + sectionAdapter.setSections(sections.toArray(dummy)); + recyclerView.setAdapter(sectionAdapter); + } + + private void startFeature(Feature feature) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(getPackageName(), feature.getName())); + startActivity(intent); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelableArrayList(KEY_STATE_FEATURES, (ArrayList) features); + } + + private class LoadFeatureTask extends AsyncTask> { + + @Override + protected List doInBackground(PackageInfo... params) { + List features = new ArrayList<>(); + PackageInfo app = params[0]; + + String packageName = getApplicationContext().getPackageName(); + String metaDataKey = getString(R.string.category); + for (ActivityInfo info : app.activities) { + if (info.labelRes != 0 && info.name.startsWith(packageName) + && !info.name.equals(FeatureOverviewActivity.class.getName())) { + String label = getString(info.labelRes); + String description = resolveString(info.descriptionRes); + String category = resolveMetaData(info.metaData, metaDataKey); + features.add(new Feature(info.name, label, description, category)); + } + } + + if (!features.isEmpty()) { + Comparator comparator = new Comparator() { + @Override + public int compare(Feature lhs, Feature rhs) { + int result = lhs.getCategory().compareToIgnoreCase(rhs.getCategory()); + if (result == 0) { + result = lhs.getLabel().compareToIgnoreCase(rhs.getLabel()); + } + return result; + } + }; + Collections.sort(features, comparator); + } + + return features; + } + + private String resolveMetaData(Bundle bundle, String key) { + String category = null; + if (bundle != null) { + category = bundle.getString(key); + } + return category; + } + + private String resolveString(@StringRes int stringRes) { + try { + return getString(stringRes); + } catch (Resources.NotFoundException exception) { + return "-"; + } + } + + @Override + protected void onPostExecute(List features) { + super.onPostExecute(features); + onFeaturesLoaded(features); + } + } + + private static class Feature implements Parcelable { + + private String name; + private String label; + private String description; + private String category; + + Feature(String name, String label, String description, String category) { + this.name = name; + this.label = label; + this.description = description; + this.category = category; + } + + private Feature(Parcel in) { + name = in.readString(); + label = in.readString(); + description = in.readString(); + category = in.readString(); + } + + String getName() { + return name; + } + + String getSimpleName() { + String[] split = name.split("\\."); + return split[split.length - 1]; + } + + String getLabel() { + return label != null ? label : getSimpleName(); + } + + String getDescription() { + return description != null ? description : "-"; + } + + String getCategory() { + return category; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(name); + out.writeString(label); + out.writeString(description); + out.writeString(category); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Feature createFromParcel(Parcel in) { + return new Feature(in); + } + + public Feature[] newArray(int size) { + return new Feature[size]; + } + }; + } + + private static class FeatureAdapter extends RecyclerView.Adapter { + + private List features; + + static class ViewHolder extends RecyclerView.ViewHolder { + + TextView labelView; + TextView descriptionView; + + ViewHolder(View view) { + super(view); + Typeface typeface = FontCache.get("Roboto-Regular.ttf", view.getContext()); + labelView = (TextView) view.findViewById(R.id.nameView); + labelView.setTypeface(typeface); + descriptionView = (TextView) view.findViewById(R.id.descriptionView); + descriptionView.setTypeface(typeface); + } + } + + FeatureAdapter(List features) { + this.features = features; + } + + @Override + public FeatureAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_feature, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.labelView.setText(features.get(position).getLabel()); + holder.descriptionView.setText(features.get(position).getDescription()); + } + + @Override + public int getItemCount() { + return features.size(); + } + } + + private static class FontCache { + + private static Hashtable fontCache = new Hashtable<>(); + + static Typeface get(String name, Context context) { + Typeface tf = fontCache.get(name); + if (tf == null) { + try { + tf = Typeface.createFromAsset(context.getAssets(), name); + fontCache.put(name, tf); + } catch (Exception exception) { + Timber.e("Font not found"); + } + } + return tf; + } + } + + private static class FeatureSectionAdapter extends RecyclerView.Adapter { + + private static final int SECTION_TYPE = 0; + + private final Context context; + private final SparseArray
sections; + private final RecyclerView.Adapter adapter; + + @LayoutRes + private final int sectionRes; + + @IdRes + private final int textRes; + + private boolean valid = true; + + FeatureSectionAdapter(Context ctx, int sectionResourceId, int textResourceId, + RecyclerView.Adapter baseAdapter) { + context = ctx; + sectionRes = sectionResourceId; + textRes = textResourceId; + adapter = baseAdapter; + sections = new SparseArray<>(); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + valid = adapter.getItemCount() > 0; + notifyDataSetChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + valid = adapter.getItemCount() > 0; + notifyItemRangeChanged(positionStart, itemCount); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + valid = adapter.getItemCount() > 0; + notifyItemRangeInserted(positionStart, itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + valid = adapter.getItemCount() > 0; + notifyItemRangeRemoved(positionStart, itemCount); + } + }); + } + + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int typeView) { + if (typeView == SECTION_TYPE) { + final View view = LayoutInflater.from(context).inflate(sectionRes, parent, false); + return new SectionViewHolder(view, textRes); + } else { + return adapter.onCreateViewHolder(parent, typeView - 1); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder sectionViewHolder, int position) { + if (isSectionHeaderPosition(position)) { + String cleanTitle = sections.get(position).title.toString().replace("_", " "); + ((SectionViewHolder) sectionViewHolder).title.setText(cleanTitle); + } else { + adapter.onBindViewHolder(sectionViewHolder, getConvertedPosition(position)); + } + } + + @Override + public int getItemViewType(int position) { + return isSectionHeaderPosition(position) + ? SECTION_TYPE + : adapter.getItemViewType(getConvertedPosition(position)) + 1; + } + + void setSections(Section[] sections) { + this.sections.clear(); + + Arrays.sort(sections, new Comparator
() { + @Override + public int compare(Section section, Section section1) { + return (section.firstPosition == section1.firstPosition) + ? 0 + : ((section.firstPosition < section1.firstPosition) ? -1 : 1); + } + }); + + int offset = 0; + for (Section section : sections) { + section.sectionedPosition = section.firstPosition + offset; + this.sections.append(section.sectionedPosition, section); + ++offset; + } + + notifyDataSetChanged(); + } + + int getConvertedPosition(int sectionedPosition) { + if (isSectionHeaderPosition(sectionedPosition)) { + return RecyclerView.NO_POSITION; + } + + int offset = 0; + for (int i = 0; i < sections.size(); i++) { + if (sections.valueAt(i).sectionedPosition > sectionedPosition) { + break; + } + --offset; + } + return sectionedPosition + offset; + } + + boolean isSectionHeaderPosition(int position) { + return sections.get(position) != null; + } + + + @Override + public long getItemId(int position) { + return isSectionHeaderPosition(position) + ? Integer.MAX_VALUE - sections.indexOfKey(position) + : adapter.getItemId(getConvertedPosition(position)); + } + + @Override + public int getItemCount() { + return (valid ? adapter.getItemCount() + sections.size() : 0); + } + } + + private static class SectionViewHolder extends RecyclerView.ViewHolder { + + private TextView title; + + SectionViewHolder(@NonNull View view, @IdRes int textRes) { + super(view); + title = (TextView) view.findViewById(textRes); + title.setTypeface(FontCache.get("Roboto-Medium.ttf", view.getContext())); + } + } + + private static class Section { + int firstPosition; + int sectionedPosition; + CharSequence title; + + public Section(int firstPosition, CharSequence title) { + this.firstPosition = firstPosition; + this.title = title; + } + + public CharSequence getTitle() { + return title; + } + } + + private static class ItemClickSupport { + private final RecyclerView recyclerView; + private OnItemClickListener onItemClickListener; + private View.OnClickListener onClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (onItemClickListener != null) { + RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view); + onItemClickListener.onItemClicked(recyclerView, holder.getAdapterPosition(), view); + } + } + }; + private RecyclerView.OnChildAttachStateChangeListener attachListener + = new RecyclerView.OnChildAttachStateChangeListener() { + @Override + public void onChildViewAttachedToWindow(View view) { + if (onItemClickListener != null) { + view.setOnClickListener(onClickListener); + } + } + + @Override + public void onChildViewDetachedFromWindow(View view) { + + } + }; + + private ItemClickSupport(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + this.recyclerView.setTag(R.id.item_click_support, this); + this.recyclerView.addOnChildAttachStateChangeListener(attachListener); + } + + static ItemClickSupport addTo(RecyclerView view) { + ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support); + if (support == null) { + support = new ItemClickSupport(view); + } + return support; + } + + ItemClickSupport setOnItemClickListener(OnItemClickListener listener) { + onItemClickListener = listener; + return this; + } + + interface OnItemClickListener { + + void onItemClicked(RecyclerView recyclerView, int position, View view); + } + } +} \ No newline at end of file diff --git a/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/TrafficActivity.java b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/TrafficActivity.java new file mode 100644 index 000000000..edc786a68 --- /dev/null +++ b/plugins/app/src/main/java/com/mapbox/mapboxsdk/plugins/testapp/activity/TrafficActivity.java @@ -0,0 +1,134 @@ +package com.mapbox.mapboxsdk.plugins.testapp.activity; + +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AppCompatActivity; + +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.plugins.testapp.R; +import com.mapbox.mapboxsdk.plugins.traffic.TrafficPlugin; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import timber.log.Timber; + +/** + * Activity showcasing TrafficPlugin plugin integration + */ +public class TrafficActivity extends AppCompatActivity implements OnMapReadyCallback { + + @BindView(R.id.mapView) + MapView mapView; + + @BindView(R.id.fabStyles) + FloatingActionButton stylesFab; + + @BindView(R.id.fabTraffic) + FloatingActionButton trafficFab; + + private MapboxMap mapboxMap; + private TrafficPlugin trafficPlugin; + private StyleCycle styleCycle = new StyleCycle(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_traffic); + ButterKnife.bind(this); + + mapView.setStyleUrl(styleCycle.getStyle()); + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(this); + } + + @Override + public void onMapReady(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + this.trafficPlugin = new TrafficPlugin(mapView, mapboxMap); + } + + @OnClick(R.id.fabTraffic) + public void onTrafficFabClick() { + if (mapboxMap != null) { + trafficPlugin.toggle(); + Timber.e("Traffic plugin is enabled :%s", trafficPlugin.isEnabled()); + } + } + + @OnClick(R.id.fabStyles) + public void onStyleFabClick() { + if (mapboxMap != null) { + mapboxMap.setStyleUrl(styleCycle.getNextStyle()); + } + } + + @Override + protected void onStart() { + super.onStart(); + mapView.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + mapView.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + mapView.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + mapView.onStop(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mapView.onSaveInstanceState(outState); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mapView.onDestroy(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + mapView.onLowMemory(); + } + + private static class StyleCycle { + private static final String[] STYLES = new String[] { + Style.MAPBOX_STREETS, + Style.OUTDOORS, + Style.LIGHT, + Style.DARK, + Style.SATELLITE_STREETS + }; + + private int index; + + private String getNextStyle() { + index++; + if (index == STYLES.length) { + index = 0; + } + return getStyle(); + } + + private String getStyle() { + return STYLES[index]; + } + } +} \ No newline at end of file diff --git a/plugins/app/src/main/res/drawable/ic_car.xml b/plugins/app/src/main/res/drawable/ic_car.xml new file mode 100644 index 000000000..6d6337c3a --- /dev/null +++ b/plugins/app/src/main/res/drawable/ic_car.xml @@ -0,0 +1,9 @@ + + + diff --git a/plugins/app/src/main/res/drawable/ic_layers.xml b/plugins/app/src/main/res/drawable/ic_layers.xml new file mode 100644 index 000000000..84f5bb50a --- /dev/null +++ b/plugins/app/src/main/res/drawable/ic_layers.xml @@ -0,0 +1,9 @@ + + + diff --git a/plugins/app/src/main/res/drawable/ic_layers_clear.xml b/plugins/app/src/main/res/drawable/ic_layers_clear.xml new file mode 100644 index 000000000..ef2778257 --- /dev/null +++ b/plugins/app/src/main/res/drawable/ic_layers_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/plugins/app/src/main/res/drawable/line_divider.xml b/plugins/app/src/main/res/drawable/line_divider.xml new file mode 100644 index 000000000..28258bddd --- /dev/null +++ b/plugins/app/src/main/res/drawable/line_divider.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/app/src/main/res/layout/activity_feature_overview.xml b/plugins/app/src/main/res/layout/activity_feature_overview.xml index 3debee030..f917189a7 100644 --- a/plugins/app/src/main/res/layout/activity_feature_overview.xml +++ b/plugins/app/src/main/res/layout/activity_feature_overview.xml @@ -1,19 +1,14 @@ - + android:orientation="vertical"> - + - + \ No newline at end of file diff --git a/plugins/app/src/main/res/layout/activity_traffic.xml b/plugins/app/src/main/res/layout/activity_traffic.xml new file mode 100644 index 000000000..b603e9254 --- /dev/null +++ b/plugins/app/src/main/res/layout/activity_traffic.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/plugins/app/src/main/res/layout/item_feature.xml b/plugins/app/src/main/res/layout/item_feature.xml new file mode 100644 index 000000000..d2c493b2e --- /dev/null +++ b/plugins/app/src/main/res/layout/item_feature.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/app/src/main/res/layout/section_feature.xml b/plugins/app/src/main/res/layout/section_feature.xml new file mode 100644 index 000000000..d1e1cb7b5 --- /dev/null +++ b/plugins/app/src/main/res/layout/section_feature.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/app/src/main/res/values/ids.xml b/plugins/app/src/main/res/values/ids.xml new file mode 100644 index 000000000..a96780447 --- /dev/null +++ b/plugins/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/plugins/app/src/main/res/values/strings.xml b/plugins/app/src/main/res/values/strings.xml index 8193e8b8a..e7d8b5562 100644 --- a/plugins/app/src/main/res/values/strings.xml +++ b/plugins/app/src/main/res/values/strings.xml @@ -1,3 +1,17 @@ Mapbox Android Plugins + + + category + Navigation + + + Traffic Plugin + + + Add Traffic layers to any Mapbox basemap. + + + pk.eyJ1IjoiY2FtbWFjZSIsImEiOiJjaW9vbGtydnQwMDAwdmRrcWlpdDVoM3pjIn0.Oy_gHelWnV12kJxHQWV7XQ + diff --git a/plugins/app/src/test/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleUnitTest.java b/plugins/app/src/test/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleUnitTest.java deleted file mode 100644 index 919eb744b..000000000 --- a/plugins/app/src/test/java/com/mapbox/mapboxsdk/plugins/testapp/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.mapbox.mapboxsdk.plugins.testapp; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/plugins/build.gradle b/plugins/build.gradle index 184a0932b..5e0e0d57b 100644 --- a/plugins/build.gradle +++ b/plugins/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.4.0-alpha5' } } diff --git a/plugins/gradle/wrapper/gradle-wrapper.properties b/plugins/gradle/wrapper/gradle-wrapper.properties index db8c6b0f8..950cb1862 100644 --- a/plugins/gradle/wrapper/gradle-wrapper.properties +++ b/plugins/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 05 09:15:49 EDT 2017 +#Mon Apr 10 12:43:59 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/plugins/settings.gradle b/plugins/settings.gradle index e7b4def49..d471af303 100644 --- a/plugins/settings.gradle +++ b/plugins/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app', ':traffic' diff --git a/plugins/traffic/.gitignore b/plugins/traffic/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/plugins/traffic/.gitignore @@ -0,0 +1 @@ +/build diff --git a/plugins/traffic/build.gradle b/plugins/traffic/build.gradle new file mode 100644 index 000000000..0baf3680b --- /dev/null +++ b/plugins/traffic/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 25 + versionCode 1 + versionName "0.1" + } +} + +dependencies { + compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.0.2@aar'){ + transitive=true + } +} diff --git a/plugins/traffic/src/main/AndroidManifest.xml b/plugins/traffic/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7987dc347 --- /dev/null +++ b/plugins/traffic/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/plugins/traffic/src/main/java/com/mapbox/mapboxsdk/plugins/traffic/TrafficPlugin.java b/plugins/traffic/src/main/java/com/mapbox/mapboxsdk/plugins/traffic/TrafficPlugin.java new file mode 100644 index 000000000..355625150 --- /dev/null +++ b/plugins/traffic/src/main/java/com/mapbox/mapboxsdk/plugins/traffic/TrafficPlugin.java @@ -0,0 +1,235 @@ +package com.mapbox.mapboxsdk.plugins.traffic; + +import android.graphics.Color; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.style.functions.CameraFunction; +import com.mapbox.mapboxsdk.style.functions.Function; +import com.mapbox.mapboxsdk.style.functions.stops.Stop; +import com.mapbox.mapboxsdk.style.layers.Filter; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.LineLayer; +import com.mapbox.mapboxsdk.style.sources.Source; +import com.mapbox.mapboxsdk.style.sources.VectorSource; + +import java.util.List; + +import static com.mapbox.mapboxsdk.style.functions.Function.zoom; +import static com.mapbox.mapboxsdk.style.functions.stops.Stop.stop; +import static com.mapbox.mapboxsdk.style.functions.stops.Stops.categorical; +import static com.mapbox.mapboxsdk.style.functions.stops.Stops.exponential; +import static com.mapbox.mapboxsdk.style.layers.Filter.in; +import static com.mapbox.mapboxsdk.style.layers.Filter.notIn; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineOffset; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; + +/** + * The traffic plugin allows to add Mapbox Traffic v1 to the Mapbox Android SDK v5.0.2. + *

+ * Initialise this plugin in the {@link com.mapbox.mapboxsdk.maps.OnMapReadyCallback#onMapReady(MapboxMap)} and provide + * a valid instance of {@link MapView} and {@link MapboxMap}. + *

+ *

+ * Use {@link #toggle()} to switch state of this plugin to enable or disabled. + * Use {@link #isEnabled()} to validate if the plugin is active or not. + *

+ */ +public final class TrafficPlugin implements MapView.OnMapChangedListener { + + private MapboxMap mapboxMap; + private boolean enabled; + + /** + * Create a traffic plugin. + * + * @param mapView the MapView to apply the traffic plugin to + * @param mapboxMap the MapboxMap to apply traffic plugin with + */ + public TrafficPlugin(@NonNull MapView mapView, @NonNull MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + mapView.addOnMapChangedListener(this); + } + + /** + * Returns true if the traffic plugin is currently enabled. + * + * @return true if enabled, false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Toggles the traffic plugin state. + *

+ * If the traffic plugin wasn't initialised yet, traffic source and layers will be added to the current map style. + * Else visibility will be toggled based on the current state. + *

+ */ + public void toggle() { + enabled = !enabled; + updateState(); + } + + /** + * Called when a map change events occurs. + *

+ * Used to detect loading of a new style, if applicable reapply traffic source and layers. + *

+ * + * @param change the map change event that occurred + */ + @Override + public void onMapChanged(int change) { + if (change == MapView.DID_FINISH_LOADING_STYLE && isEnabled()) { + updateState(); + } + } + + /** + * Update the state of the traffic plugin. + */ + private void updateState() { + Source source = mapboxMap.getSource(SourceData.SOURCE_ID); + if (source == null) { + initSourceAndLayers(); + return; + } + setVisibility(enabled); + } + + /** + * Initialise the traffic source and layers + */ + private void initSourceAndLayers() { + // source + VectorSource trafficSource = new VectorSource(SourceData.SOURCE_ID, SourceData.SOURCE_URL); + mapboxMap.addSource(trafficSource); + + // functions + Function lineColor = TrafficFunction.getLineColorFunction(TrafficColor.BASE_GREEN, TrafficColor.BASE_YELLOW, TrafficColor.BASE_ORANGE, TrafficColor.BASE_RED); + Function lineColorCase = TrafficFunction.getLineColorFunction(TrafficColor.CASE_GREEN, TrafficColor.CASE_YELLOW, TrafficColor.CASE_ORANGE, TrafficColor.CASE_RED); + Function motorwayOffset = TrafficFunction.getOffsetFunction(stop(5, lineOffset(0.5f)), stop(13, lineOffset(3.0f)), stop(18, lineOffset(7.0f))); + Function otherOffset = TrafficFunction.getOffsetFunction(stop(7, lineOffset(0.3f)), stop(18, lineOffset(6.0f)), stop(22, lineOffset(100.0f))); + CameraFunction motorwayWidth = TrafficFunction.getWidthFunction(stop(7, lineWidth(1.5f)), stop(18, lineWidth(20.0f))); + CameraFunction motorwayCaseWidth = TrafficFunction.getWidthFunction(stop(7, lineWidth(3.0f)), stop(18, lineWidth(24.0f))); + CameraFunction otherWidth = TrafficFunction.getWidthFunction(stop(11, lineWidth(1.0f)), stop(14, lineWidth(2.0f)), stop(17, lineWidth(4.0f)), stop(22, lineWidth(30.0f))); + CameraFunction otherCaseWidth = TrafficFunction.getWidthFunction(stop(11, lineWidth(1.25f)), stop(14, lineWidth(2.5f)), stop(17, lineWidth(5.5f)), stop(22, lineWidth(34.0f))); + + // layers + LineLayer motorWay = TrafficLayer.getLineLayer(MotorWay.BASE_LAYER_ID, lineColor, motorwayWidth, motorwayOffset, MotorWay.ZOOM_LEVEL, MotorWay.FILTER); + LineLayer motorwayCase = TrafficLayer.getLineLayer(MotorWay.CASE_LAYER_ID, lineColorCase, motorwayCaseWidth, motorwayOffset, MotorWay.ZOOM_LEVEL, MotorWay.FILTER); + LineLayer primary = TrafficLayer.getLineLayer(Primary.BASE_LAYER_ID, lineColor, otherWidth, otherOffset, Primary.ZOOM_LEVEL, Primary.FILTER); + LineLayer primaryCase = TrafficLayer.getLineLayer(Primary.CASE_LAYER_ID, lineColorCase, otherCaseWidth, otherOffset, Primary.ZOOM_LEVEL, Primary.FILTER); + LineLayer local = TrafficLayer.getLineLayer(Local.BASE_LAYER_ID, lineColor, otherWidth, otherOffset, Local.ZOOM_LEVEL, Local.FILTER); + LineLayer localCase = TrafficLayer.getLineLayer(Local.LOCAL_CASE_LAYER_ID, lineColorCase, otherCaseWidth, otherOffset, Local.ZOOM_LEVEL, Local.FILTER); + + // // TODO: add above highest road label instead of bridge-motorway https://github.com/mapbox/mapbox-gl-native/issues/8663 + mapboxMap.addLayerAbove(localCase, "bridge-motorway"); + mapboxMap.addLayerAbove(local, localCase.getId()); + mapboxMap.addLayerAbove(primaryCase, local.getId()); + mapboxMap.addLayerAbove(primary, primaryCase.getId()); + mapboxMap.addLayerAbove(motorwayCase, primary.getId()); + mapboxMap.addLayerAbove(motorWay, motorwayCase.getId()); + } + + /** + * Toggles the visibility of the traffic layers. + * + * @param visible true for visible, false for none + */ + private void setVisibility(boolean visible) { + List layers = mapboxMap.getLayers(); + String id; + for (Layer layer : layers) { + id = layer.getId(); + // TODO use sourceLayer filter instead + if (id.equals(MotorWay.BASE_LAYER_ID) || id.equals(MotorWay.CASE_LAYER_ID) || id.equals(Primary.BASE_LAYER_ID) + || id.equals(Primary.CASE_LAYER_ID) || id.equals(Primary.BASE_LAYER_ID) || id.equals(Primary.CASE_LAYER_ID)) { + layer.setProperties(visibility(visible ? "visible" : "none")); + } + } + } + + private static class TrafficFunction { + private static Function getLineColorFunction(@ColorInt int low, @ColorInt int moderate, @ColorInt int heavy, @ColorInt int severe) { + return Function.property( + "congestion", + categorical( + stop("low", fillColor(low)), + stop("moderate", fillColor(moderate)), + stop("heavy", fillColor(heavy)), + stop("severe", fillColor(severe)) + ) + ).withDefaultValue(fillColor(Color.TRANSPARENT)); + } + + private static CameraFunction getOffsetFunction(Stop... stops) { + return zoom(exponential(stops).withBase(1.5f)); + } + + private static CameraFunction getWidthFunction(Stop... stops) { + return zoom(exponential(stops).withBase(1.5f)); + } + } + + private static class TrafficLayer { + + private static LineLayer getLineLayer(String lineLayerId, Function lineColor, CameraFunction lineWidth, Function lineOffset, float minZoom, Filter.Statement statement) { + LineLayer lineLayer = new LineLayer(lineLayerId, SourceData.SOURCE_ID); + lineLayer.setSourceLayer(SourceData.SOURCE_LAYER); + lineLayer.setMinZoom(minZoom); + lineLayer.setProperties( + lineColor(lineColor), + lineWidth(lineWidth), + lineOffset(lineOffset) + ); + lineLayer.setFilter(statement); + return lineLayer; + } + } + + private static class SourceData { + private static final String SOURCE_ID = "traffic"; + private static final String SOURCE_LAYER = "traffic"; + private static final String SOURCE_URL = "mapbox://mapbox.mapbox-traffic-v1"; + } + + private static class MotorWay { + private static final String BASE_LAYER_ID = "traffic-motorway"; + private static final String CASE_LAYER_ID = "traffic-motorway-case"; + private static final float ZOOM_LEVEL = 5.0f; + private static final Filter.Statement FILTER = in("class", "motorway", "trunk"); + } + + private static class Primary { + private static final String BASE_LAYER_ID = "traffic-primary"; + private static final String CASE_LAYER_ID = "traffic-primary-case"; + private static final float ZOOM_LEVEL = 5.0f; + private static final Filter.Statement FILTER = notIn("class", "motorway", "trunk", "service", "street"); + } + + private static class Local { + private static final String BASE_LAYER_ID = "traffic-local"; + private static final String LOCAL_CASE_LAYER_ID = "traffic-local-case"; + private static final float ZOOM_LEVEL = 16.0f; + private static final Filter.Statement FILTER = in("class", "street"); + } + + private static class TrafficColor { + private static final int BASE_GREEN = Color.parseColor("#4CAF50"); + private static final int CASE_GREEN = Color.parseColor("#388E3C"); + private static final int BASE_YELLOW = Color.parseColor("#FFEB3B"); + private static final int CASE_YELLOW = Color.parseColor("#FBC02D"); + private static final int BASE_ORANGE = Color.parseColor("#FF9800"); + private static final int CASE_ORANGE = Color.parseColor("#F57C00"); + private static final int BASE_RED = Color.parseColor("#f44336"); + private static final int CASE_RED = Color.parseColor("#D32F2F"); + } +}