package org.mtransit.android.ui.fragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.mtransit.android.R;
import org.mtransit.android.commons.BundleUtils;
import org.mtransit.android.commons.LocationUtils;
import org.mtransit.android.commons.MTLog;
import org.mtransit.android.commons.PreferenceUtils;
import org.mtransit.android.commons.task.MTAsyncTask;
import org.mtransit.android.commons.TaskUtils;
import org.mtransit.android.data.DataSourceProvider;
import org.mtransit.android.data.DataSourceType;
import org.mtransit.android.task.MapPOILoader;
import org.mtransit.android.ui.MTActivityWithLocation;
import org.mtransit.android.ui.view.MapViewController;
import org.mtransit.android.util.LoaderUtils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
public class MapFragment extends ABFragment implements LoaderManager.LoaderCallbacks<Collection<MapViewController.POIMarker>>,
MTActivityWithLocation.UserLocationListener, MapViewController.MapListener {
private static final String TAG = MapFragment.class.getSimpleName();
@Override
public String getLogTag() {
return TAG;
}
private static final String TRACKING_SCREEN_NAME = "Map";
@Override
public String getScreenName() {
return TRACKING_SCREEN_NAME;
}
private static final String EXTRA_INITIAL_LOCATION = "extra_initial_location";
private static final String EXTRA_SELECTED_UUID = "extra_selected_uuid";
private static final String EXTRA_INCLUDE_TYPE_ID = "extra_include_type_id";
public static MapFragment newInstance(Location optInitialLocation, String optSelectedUUID, Integer optIncludeTypeId) {
MapFragment f = new MapFragment();
Bundle args = new Bundle();
if (optInitialLocation != null) {
args.putParcelable(EXTRA_INITIAL_LOCATION, optInitialLocation);
}
if (!TextUtils.isEmpty(optSelectedUUID)) {
args.putString(EXTRA_SELECTED_UUID, optSelectedUUID);
}
if (optIncludeTypeId != null) {
args.putInt(EXTRA_INCLUDE_TYPE_ID, optIncludeTypeId);
f.includedTypeId = optIncludeTypeId;
}
f.setArguments(args);
return f;
}
private MapViewController mapViewController = new MapViewController(TAG, null, this, true, true, true, false, false, false, 64, false, true, true, false,
true);
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.mapViewController.onAttach(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
restoreInstanceState(savedInstanceState, getArguments());
this.mapViewController.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_map, container, false);
this.mapViewController.onCreateView(view, savedInstanceState);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupView(view);
this.mapViewController.onViewCreated(view, savedInstanceState);
}
private void setupView(View view) {
}
private void restoreInstanceState(Bundle... bundles) {
Location newInitialLocation = BundleUtils.getParcelable(EXTRA_INITIAL_LOCATION, bundles);
if (newInitialLocation != null) {
this.mapViewController.setInitialLocation(newInitialLocation);
}
String newSelectedUUID = BundleUtils.getString(EXTRA_SELECTED_UUID, bundles);
if (!TextUtils.isEmpty(newSelectedUUID)) {
this.mapViewController.setInitialSelectedUUID(newSelectedUUID);
}
Integer newIncludedTypeId = BundleUtils.getInt(EXTRA_INCLUDE_TYPE_ID, bundles);
if (newIncludedTypeId != null) {
this.includedTypeId = newIncludedTypeId;
}
this.mapViewController.setTag(getLogTag());
}
@Override
public void onResume() {
super.onResume();
View view = getView();
if (this.modulesUpdated) {
if (view != null) {
view.post(new Runnable() {
@Override
public void run() {
if (MapFragment.this.modulesUpdated) {
onModulesUpdated();
}
}
});
}
}
this.mapViewController.onResume();
hasFilterTypeIds(); // triggers markers loading if necessary
this.mapViewController.showMap(view);
onUserLocationChanged(((MTActivityWithLocation) getActivity()).getLastLocation());
}
private boolean modulesUpdated = false;
@Override
public void onModulesUpdated() {
this.modulesUpdated = true;
if (!isResumed()) {
return;
}
if (hasFilterTypeIds()) {
resetTypeFilterIds();
initFilterTypeIdsAsync();
}
this.modulesUpdated = false; // processed
}
private Location userLocation;
@Override
public void onUserLocationChanged(Location newLocation) {
if (newLocation == null) {
return;
}
if (this.userLocation == null || LocationUtils.isMoreRelevant(getLogTag(), this.userLocation, newLocation)) {
this.userLocation = newLocation;
}
this.mapViewController.onUserLocationChanged(newLocation);
}
private static final int POIS_LOADER = 0;
@Override
public Loader<Collection<MapViewController.POIMarker>> onCreateLoader(int id, Bundle args) {
switch (id) {
case POIS_LOADER:
return new MapPOILoader(getActivity(), getFilterTypeIdsOrNull(), this.loadingLatLngBounds, this.loadedLatLngBounds);
default:
MTLog.w(this, "Loader id '%s' unknown!", id);
return null;
}
}
@Override
public void onLoaderReset(Loader<Collection<MapViewController.POIMarker>> loader) {
}
@Override
public void onLoadFinished(Loader<Collection<MapViewController.POIMarker>> loader, Collection<MapViewController.POIMarker> data) {
if (this.loadingLatLngBounds == null) {
return;
}
if (this.loadedLatLngBounds != null) {
this.mapViewController.addMarkers(data);
this.loadedLatLngBounds = this.loadingLatLngBounds;
this.loadingLatLngBounds = null;
this.mapViewController.hideLoading();
} else {
this.mapViewController.addMarkers(data);
this.loadedLatLngBounds = this.loadingLatLngBounds;
this.loadingLatLngBounds = null;
this.mapViewController.showMap(getView());
}
}
private LatLngBounds loadingLatLngBounds;
private LatLngBounds loadedLatLngBounds;
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.mapViewController.onConfigurationChanged(newConfig);
}
@Override
public void onMapClick(LatLng position) {
}
@Override
public void onCameraChange(LatLngBounds latLngBounds) {
if (latLngBounds == null) {
return;
}
boolean loaded = this.loadedLatLngBounds != null && this.loadedLatLngBounds.contains(latLngBounds.northeast)
&& this.loadedLatLngBounds.contains(latLngBounds.southwest);
boolean loading = this.loadingLatLngBounds != null && this.loadingLatLngBounds.contains(latLngBounds.northeast)
&& this.loadingLatLngBounds.contains(latLngBounds.southwest);
if (!loaded && !loading) {
this.mapViewController.showLoading();
getLoaderManager().destroyLoader(POIS_LOADER); // cancel now
if (this.loadingLatLngBounds != null) {
if (!this.loadingLatLngBounds.contains(latLngBounds.northeast)) {
this.loadingLatLngBounds = this.loadingLatLngBounds.including(latLngBounds.northeast);
}
if (!this.loadingLatLngBounds.contains(latLngBounds.southwest)) {
this.loadingLatLngBounds = this.loadingLatLngBounds.including(latLngBounds.southwest);
}
} else if (this.loadedLatLngBounds != null) {
this.loadingLatLngBounds = this.loadedLatLngBounds;
if (!this.loadingLatLngBounds.contains(latLngBounds.northeast)) {
this.loadingLatLngBounds = this.loadingLatLngBounds.including(latLngBounds.northeast);
}
if (!this.loadingLatLngBounds.contains(latLngBounds.southwest)) {
this.loadingLatLngBounds = this.loadingLatLngBounds.including(latLngBounds.southwest);
}
} else {
this.loadingLatLngBounds = latLngBounds;
float factor = 1.0f;
LatLngBounds bigLatLngBounds = this.mapViewController.getBigCameraPosition(getActivity(), factor);
if (bigLatLngBounds != null) {
this.loadingLatLngBounds = bigLatLngBounds;
}
}
if (hasFilterTypeIds()) {
LoaderUtils.restartLoader(this, POIS_LOADER, null, this);
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_map, menu);
}
private Integer includedTypeId = null;
private Set<Integer> filterTypeIds = null;
private void resetTypeFilterIds() {
this.filterTypeIds = null; // reset
}
private Set<Integer> getFilterTypeIdsOrNull() {
if (!hasFilterTypeIds()) {
return null;
}
return this.filterTypeIds;
}
private boolean hasFilterTypeIds() {
if (this.filterTypeIds == null) {
initFilterTypeIdsAsync();
return false;
}
return true;
}
private void initFilterTypeIdsAsync() {
if (this.loadFilterTypeIdsTask != null && this.loadFilterTypeIdsTask.getStatus() == MTAsyncTask.Status.RUNNING) {
return;
}
this.loadFilterTypeIdsTask = new LoadFilterTypeIdsTask();
TaskUtils.execute(this.loadFilterTypeIdsTask);
}
private LoadFilterTypeIdsTask loadFilterTypeIdsTask = null;
private class LoadFilterTypeIdsTask extends MTAsyncTask<Object, Void, Boolean> {
@Override
public String getLogTag() {
return MapFragment.this.getLogTag() + ">" + LoadFilterTypeIdsTask.class.getSimpleName();
}
@Override
protected Boolean doInBackgroundMT(Object... params) {
return initFilterTypeIdsSync();
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
applyNewFilterTypeIds();
}
}
}
private boolean initFilterTypeIdsSync() {
if (this.filterTypeIds != null) {
return false;
}
ArrayList<DataSourceType> availableTypes = filterTypes(DataSourceProvider.get(getActivity()).getAvailableAgencyTypes());
Set<String> filterTypeIdStrings = PreferenceUtils.getPrefLcl(getActivity(), PreferenceUtils.PREFS_LCL_MAP_FILTER_TYPE_IDS,
PreferenceUtils.PREFS_LCL_MAP_FILTER_TYPE_IDS_DEFAULT);
this.filterTypeIds = new HashSet<Integer>();
boolean hasChanged = false;
for (String typeIdString : filterTypeIdStrings) {
try {
DataSourceType type = DataSourceType.parseId(Integer.parseInt(typeIdString));
if (type == null) {
hasChanged = true;
continue;
}
if (!availableTypes.contains(type)) {
hasChanged = true;
continue;
}
this.filterTypeIds.add(type.getId());
} catch (Exception e) {
MTLog.w(this, e, "Error while parsing filter type ID '%s'!", typeIdString);
hasChanged = true;
}
}
if (this.includedTypeId != null) {
if (this.filterTypeIds.size() > 0 && !this.filterTypeIds.contains(this.includedTypeId)) {
try {
DataSourceType type = DataSourceType.parseId(this.includedTypeId);
if (type == null) {
} else if (!availableTypes.contains(type)) {
} else {
this.filterTypeIds.add(type.getId());
hasChanged = true;
}
} catch (Exception e) {
MTLog.w(this, e, "Error while parsing filter type ID '%s'!", this.includedTypeId);
hasChanged = true;
}
}
this.includedTypeId = null; // only once
}
if (hasChanged) { // old setting not valid anymore
saveMapFilterTypeIdsSetting(false); // asynchronous
}
return this.filterTypeIds != null;
}
private ArrayList<DataSourceType> filterTypes(ArrayList<DataSourceType> availableTypes) {
Iterator<DataSourceType> it = availableTypes.iterator();
while (it.hasNext()) {
if (!it.next().isMapScreen()) {
it.remove();
}
}
return availableTypes;
}
private void saveMapFilterTypeIdsSetting(boolean sync) {
Set<Integer> filterTypeIds = getFilterTypeIdsOrNull();
if (filterTypeIds == null) {
return;
}
Set<String> newFilterTypeIdStrings = new HashSet<String>();
for (Integer filterTypeId : filterTypeIds) {
newFilterTypeIdStrings.add(String.valueOf(filterTypeId));
}
PreferenceUtils.savePrefLcl(getActivity(), PreferenceUtils.PREFS_LCL_MAP_FILTER_TYPE_IDS, newFilterTypeIdStrings, sync);
}
private void applyNewFilterTypeIds() {
if (this.filterTypeIds == null) {
return;
}
getLoaderManager().destroyLoader(POIS_LOADER); // cancel now
getAbController().setABTitle(this, getABTitle(getActivity()), true);
if (this.loadingLatLngBounds == null) {
this.loadingLatLngBounds = this.loadedLatLngBounds; // use the loaded area
}
this.loadedLatLngBounds = null; // loaded with wrong filter
this.mapViewController.clearMarkers();
this.mapViewController.showMap(getView());
if (this.loadingLatLngBounds != null) {
LoaderUtils.restartLoader(this, POIS_LOADER, null, this);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_filter:
Set<Integer> filterTypeIds = getFilterTypeIdsOrNull();
if (filterTypeIds == null) {
return false;
}
ArrayList<CharSequence> typeNames = new ArrayList<CharSequence>();
ArrayList<Boolean> checked = new ArrayList<Boolean>();
final ArrayList<Integer> typeIds = new ArrayList<Integer>();
final HashSet<Integer> selectedItems = new HashSet<Integer>();
ArrayList<DataSourceType> availableAgencyTypes = filterTypes(DataSourceProvider.get(getActivity()).getAvailableAgencyTypes());
for (DataSourceType type : availableAgencyTypes) {
typeIds.add(type.getId());
typeNames.add(getString(type.getPoiShortNameResId()));
checked.add(filterTypeIds.size() == 0 || filterTypeIds.contains(type.getId()));
}
boolean[] checkedItems = new boolean[checked.size()];
for (int c = 0; c < checked.size(); c++) {
checkedItems[c] = checked.get(c);
if (checkedItems[c]) {
selectedItems.add(c);
}
}
new AlertDialog.Builder(getActivity()) //
.setTitle(R.string.menu_action_filter) //
.setMultiChoiceItems(typeNames.toArray(new CharSequence[typeNames.size()]), checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if (isChecked) {
selectedItems.add(which);
} else {
selectedItems.remove(which);
}
}
}) //
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
applyNewFilter(typeIds, selectedItems);
dialog.dismiss();
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).setCancelable(true).show();
return true; // handled
}
return super.onOptionsItemSelected(item);
}
@Override
public void onPause() {
super.onPause();
this.mapViewController.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
this.mapViewController.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
@Override
public void onLowMemory() {
super.onLowMemory();
this.mapViewController.onLowMemory();
}
@Override
public void onDestroyView() {
super.onDestroyView();
this.mapViewController.onDestroyView();
this.loadedLatLngBounds = null;
}
@Override
public void onDestroy() {
super.onDestroy();
this.mapViewController.onDestroy();
TaskUtils.cancelQuietly(this.loadFilterTypeIdsTask, true);
}
@Override
public void onDetach() {
super.onDetach();
this.mapViewController.onDetach();
}
@Override
public CharSequence getABTitle(Context context) {
StringBuilder sb = new StringBuilder(context.getString(R.string.map));
sb.append(" (");
Set<Integer> filterTypeIds = getFilterTypeIdsOrNull();
if (filterTypeIds != null && filterTypeIds.size() > 0) {
boolean hasType = false;
for (Integer typeId : filterTypeIds) {
if (typeId != null) {
DataSourceType type = DataSourceType.parseId(typeId);
if (type != null) {
if (hasType) {
sb.append(", ");
}
sb.append(getString(type.getAllStringResId()));
hasType = true;
}
}
}
} else {
sb.append(context.getString(R.string.all));
}
sb.append(")");
return sb.toString();
}
@Override
public Integer getABBgColor(Context context) {
return Color.TRANSPARENT;
}
private void applyNewFilter(ArrayList<Integer> typeIds, HashSet<Integer> selectedItems) {
boolean filterChanged = false;
if (this.filterTypeIds == null) {
return;
}
if (selectedItems.size() == 0 || selectedItems.size() == typeIds.size()) {
if (this.filterTypeIds.size() > 0) {
this.filterTypeIds.clear();
filterChanged = true;
}
} else {
for (int i = 0; i < typeIds.size(); i++) {
Integer typeId = typeIds.get(i);
if (selectedItems.contains(i)) {
if (!this.filterTypeIds.contains(typeId)) {
this.filterTypeIds.add(typeId);
filterChanged = true;
}
} else {
if (this.filterTypeIds.contains(typeId)) {
this.filterTypeIds.remove(typeId);
filterChanged = true;
}
}
}
}
if (filterChanged) {
saveMapFilterTypeIdsSetting(false); // asynchronous
applyNewFilterTypeIds();
}
}
}