diff --git a/app/build.gradle b/app/build.gradle index 829c0275..8c1b3c43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,19 +62,16 @@ repositories { maven { url "https://jitpack.io" } } -configurations { - configureEach { - // https://stackoverflow.com/questions/69817925/problem-duplicate-class-androidx-lifecycle-viewmodel-found-in-modules - exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' - } -} - dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test:rules:1.5.0' + implementation "androidx.lifecycle:lifecycle-viewmodel:2.7.0" + implementation "androidx.lifecycle:lifecycle-livedata:2.7.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0" implementation "androidx.fragment:fragment:1.6.2" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.3.2' diff --git a/app/src/main/java/mobi/maptrek/MainActivity.java b/app/src/main/java/mobi/maptrek/MainActivity.java index 9c4c7979..58a05454 100644 --- a/app/src/main/java/mobi/maptrek/MainActivity.java +++ b/app/src/main/java/mobi/maptrek/MainActivity.java @@ -38,6 +38,7 @@ import android.content.pm.PermissionInfo; import android.content.res.Resources; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; @@ -80,12 +81,11 @@ import androidx.fragment.app.FragmentFactory; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.preference.PreferenceManager; import androidx.work.Data; -import androidx.work.WorkInfo; import androidx.work.WorkManager; import android.text.Html; @@ -172,6 +172,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import mobi.maptrek.data.Amenity; import mobi.maptrek.data.MapObject; import mobi.maptrek.data.Route; import mobi.maptrek.data.Track; @@ -242,6 +243,7 @@ import mobi.maptrek.location.LocationService; import mobi.maptrek.location.NavigationService; import mobi.maptrek.maps.MapWorker; +import mobi.maptrek.maps.maptrek.MapTrekDatabaseHelper; import mobi.maptrek.plugin.PluginRepository; import mobi.maptrek.util.ContextUtils; import mobi.maptrek.util.SafeResultReceiver; @@ -264,6 +266,9 @@ import mobi.maptrek.util.StringFormatter; import mobi.maptrek.util.SunriseSunset; import mobi.maptrek.view.Gauge; +import mobi.maptrek.viewmodels.AmenityViewModel; +import mobi.maptrek.viewmodels.MapIndexViewModel; +import mobi.maptrek.viewmodels.MapViewModel; public class MainActivity extends AppCompatActivity implements ILocationListener, DataHolder, @@ -389,14 +394,20 @@ private enum PANEL_STATE { private LocationOverlay mLocationOverlay; private MapCoverageLayer mMapCoverageLayer; private MarkerItem mActiveMarker; + private MarkerItem marker; + private FragmentManager mFragmentManager; private PANEL_STATE mPanelState; private boolean secondBack; private Toast mBackToast; + private AmenityViewModel amenityViewModel; + private MapIndexViewModel mapIndexViewModel; + private MapViewModel mapViewModel; + + private SQLiteDatabase mDetailedMapDatabase; private MapIndex mMapIndex; - private Index mNativeMapIndex; private MapTrekTileSource mNativeTileSource; private List mBitmapLayerMaps; private WaypointDbDataSource mWaypointDbDataSource; @@ -490,7 +501,6 @@ protected void onCreate(Bundle savedInstanceState) { mFragmentManager = getSupportFragmentManager(); mFragmentManager.registerFragmentLifecycleCallbacks(mFragmentLifecycleCallback, true); - mNativeMapIndex = application.getMapIndex(); mMapIndex = application.getExtraMapIndex(); mShieldFactory = application.getShieldFactory(); @@ -632,7 +642,8 @@ public void onChildViewRemoved(View parent, View child) { layers.addGroup(MAP_BASE); try { - mNativeTileSource = new MapTrekTileSource(application.getDetailedMapDatabase()); + mDetailedMapDatabase = application.getDetailedMapDatabase(); + mNativeTileSource = new MapTrekTileSource(mDetailedMapDatabase); mNativeTileSource.setContoursEnabled(Configuration.getContoursEnabled()); mBaseLayer = new MapTrekTileLayer(mMap, mNativeTileSource, this); mMap.setBaseMap(mBaseLayer); // will go to base group @@ -689,6 +700,37 @@ public void onChildViewRemoved(View parent, View child) { mMarkerLayer = new ItemizedLayer<>(mMap, new ArrayList<>(), symbol, MapTrek.density, this); layers.add(mMarkerLayer, MAP_3D_DATA); + mapIndexViewModel = new ViewModelProvider(this).get(MapIndexViewModel.class); + + mapViewModel = new ViewModelProvider(this).get(MapViewModel.class); + // Observe marker state + mapViewModel.getMarkerState().observe(this, markerState -> { + // There can be only one marker at a time + if (marker != null) { + mMarkerLayer.removeItem(marker); + marker.getMarker().getBitmap().recycle(); + marker = null; + } + if (markerState.isShown()) { + marker = new MarkerItem(markerState.getName(), null, markerState.getCoordinates()); + int drawable = markerState.isAmenity() ? R.drawable.circle_marker : R.drawable.round_marker; + Bitmap markerBitmap = new AndroidBitmap(MarkerFactory.getMarkerSymbol(this, drawable, mColorAccent)); + marker.setMarker(new MarkerSymbol(markerBitmap, MarkerItem.HotspotPlace.CENTER)); + mMarkerLayer.addItem(marker); + } + mMap.updateMap(true); + }); + + amenityViewModel = new ViewModelProvider(this).get(AmenityViewModel.class); + // Observe amenity state + amenityViewModel.getAmenity().observe(this, amenity -> { + if (amenity != null) { + mapViewModel.showMarker(amenity.coordinates, amenity.name, true); + } else { + mapViewModel.removeMarker(); + } + }); + // Load waypoints mWaypointDbDataSource = application.getWaypointDbDataSource(); mWaypointDbDataSource.open(); @@ -979,8 +1021,8 @@ public void onAnimationEnd(Animator animation) { ft.replace(R.id.contentPanel, fragment, "crashReport"); ft.addToBackStack("crashReport"); ft.commit(); - } else if (!mBaseMapWarningShown && mNativeMapIndex != null && mNativeMapIndex.getBaseMapVersion() == 0) { - BaseMapDownload dialogFragment = new BaseMapDownload(mNativeMapIndex); + } else if (!mBaseMapWarningShown && mapIndexViewModel.nativeIndex.getBaseMapVersion() == 0) { + BaseMapDownload dialogFragment = new BaseMapDownload(); dialogFragment.show(mFragmentManager, "baseMapDownload"); mBaseMapWarningShown = true; } else if (WhatsNewDialog.shouldShow()) { @@ -1098,6 +1140,7 @@ protected void onDestroy() { if (mWaypointDbDataSource != null) mWaypointDbDataSource.close(); + mDetailedMapDatabase = null; mProgressHandler = null; if (mBackgroundThread != null) { @@ -1398,7 +1441,7 @@ public void onDismissed(Snackbar transientBottomBar, int event) { return true; } else if (action == R.id.actionSettings) { Bundle args = new Bundle(1); - args.putBoolean(Settings.ARG_HILLSHADES_AVAILABLE, mNativeMapIndex.hasHillshades()); + args.putBoolean(Settings.ARG_HILLSHADES_AVAILABLE, mapIndexViewModel.nativeIndex.hasHillshades()); FragmentFactory factory = mFragmentManager.getFragmentFactory(); Fragment fragment = factory.instantiate(getClassLoader(), Settings.class.getName()); fragment.setArguments(args); @@ -1439,22 +1482,22 @@ public void onDismissed(Snackbar transientBottomBar, int event) { ft.commit(); return true; } else if (action == R.id.actionShareCoordinates) { - removeMarker(); + mapViewModel.removeMarker(); shareLocation(mSelectedPoint, null); return true; } else if (action == R.id.actionAddWaypointHere) { - removeMarker(); + mapViewModel.removeMarker(); String name = getString(R.string.place_name, Configuration.getPointsCounter()); onWaypointCreate(mSelectedPoint, name, false, true); return true; } else if (action == R.id.actionNavigateHere) { - removeMarker(); + mapViewModel.removeMarker(); MapObject mapObject = new MapObject(mSelectedPoint.getLatitude(), mSelectedPoint.getLongitude()); mapObject.name = getString(R.string.selectedLocation); startNavigation(mapObject); return true; } else if (action == R.id.actionFindRouteHere) { - removeMarker(); + mapViewModel.removeMarker(); Intent routeIntent = new Intent(Intent.ACTION_PICK, null, this, GraphHopperService.class); double[] points = new double[]{0.0, 0.0, 0.0, 0.0}; if (mLocationState != LocationState.DISABLED && mLocationService != null) { @@ -1470,12 +1513,12 @@ public void onDismissed(Snackbar transientBottomBar, int event) { return true; } else if (action == R.id.actionRememberScale) { HelperUtils.showTargetedAdvice(this, Configuration.ADVICE_REMEMBER_SCALE, R.string.advice_remember_scale, mViews.popupAnchor, true); - removeMarker(); + mapViewModel.removeMarker(); mMap.getMapPosition(mMapPosition); Configuration.setRememberedScale((float) mMapPosition.getScale()); return true; } else if (action == R.id.actionRememberTilt) { - removeMarker(); + mapViewModel.removeMarker(); mMap.getMapPosition(mMapPosition); mAutoTilt = mMapPosition.getTilt(); Configuration.setAutoTilt(mAutoTilt); @@ -1558,6 +1601,8 @@ else if (mAveragedBearing < bearing - 180f) mNavigationLayer.setPosition(lat, lon); mLastLocationMilliseconds = SystemClock.uptimeMillis(); + mapViewModel.setLocation(location); + // TODO: Fix lint error if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_AUTO) checkNightMode(location); @@ -1581,6 +1626,7 @@ public void onGpsStatusChanged() { mLocationState = LocationState.SEARCHING; mMap.getEventLayer().setFixOnCenter(false); mLocationOverlay.setEnabled(false); + mapViewModel.setLocation(new Location("unknown")); updateLocationDrawable(); } } @@ -1927,6 +1973,7 @@ public void disableLocations() { unbindService(mLocationConnection); mIsLocationBound = false; mLocationOverlay.setEnabled(false); + mapViewModel.setLocation(new Location("unknown")); mMap.updateMap(true); } mLocationState = LocationState.DISABLED; @@ -1950,30 +1997,6 @@ public void setMapLocation(@NonNull GeoPoint point) { } } - private MarkerItem mMarker; - - @Override - public void showMarker(@NonNull GeoPoint point, String name, boolean amenity) { - // There can be only one marker at a time - removeMarker(); - mMarker = new MarkerItem(name, null, point); - int drawable = amenity ? R.drawable.circle_marker : R.drawable.round_marker; - Bitmap bitmap = new AndroidBitmap(MarkerFactory.getMarkerSymbol(this, drawable, mColorAccent)); - mMarker.setMarker(new MarkerSymbol(bitmap, MarkerItem.HotspotPlace.CENTER)); - mMarkerLayer.addItem(mMarker); - mMap.updateMap(true); - } - - @Override - public void removeMarker() { - if (mMarker == null) - return; - mMarkerLayer.removeItem(mMarker); - mMap.updateMap(true); - mMarker.getMarker().getBitmap().recycle(); - mMarker = null; - } - @Override public void setObjectInteractionEnabled(boolean enabled) { mObjectInteractionEnabled = enabled; @@ -2086,7 +2109,7 @@ public void setHighlightedType(int type) { private void enableTracking() { Intent intent = new Intent(getApplicationContext(), LocationService.class).setAction(BaseLocationService.ENABLE_TRACK); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent); else startService(intent); @@ -2278,7 +2301,7 @@ public boolean onGesture(Gesture gesture, MotionEvent event) { mViews.popupAnchor.setX(event.getX() + mFingerTipSize); mViews.popupAnchor.setY(event.getY() - mFingerTipSize); mSelectedPoint = mMap.viewport().fromScreenPoint(event.getX(), event.getY()); - showMarker(mSelectedPoint, null, false); + mapViewModel.showMarker(mSelectedPoint, null, false); PopupMenu popup = new PopupMenu(this, mViews.popupAnchor); popup.inflate(R.menu.context_menu_map); Menu popupMenu = popup.getMenu(); @@ -2289,7 +2312,7 @@ public boolean onGesture(Gesture gesture, MotionEvent event) { if (mLocationState != LocationState.TRACK || mAutoTilt == -1f || MathUtils.equals(mAutoTilt, mMapPosition.getTilt())) popupMenu.removeItem(R.id.actionRememberTilt); popup.setOnMenuItemClickListener(this); - popup.setOnDismissListener(menu -> removeMarker()); + popup.setOnDismissListener(menu -> mapViewModel.removeMarker()); popup.show(); return true; } @@ -2563,13 +2586,9 @@ public void showMarkerInformation(@NonNull GeoPoint point, @Nullable String name if (mFragmentManager.getBackStackEntryCount() > 0) { popAll(); } - Bundle args = new Bundle(3); - args.putDouble(MarkerInformation.ARG_LATITUDE, point.getLatitude()); - args.putDouble(MarkerInformation.ARG_LONGITUDE, point.getLongitude()); - args.putString(MarkerInformation.ARG_NAME, name); + mapViewModel.showMarker(point, name, false); FragmentFactory factory = mFragmentManager.getFragmentFactory(); Fragment fragment = factory.instantiate(getClassLoader(), MarkerInformation.class.getName()); - fragment.setArguments(args); fragment.setEnterTransition(new Slide()); FragmentTransaction ft = mFragmentManager.beginTransaction(); ft.replace(R.id.contentPanel, fragment, "markerInformation"); @@ -2625,6 +2644,8 @@ public void onWaypointFocus(Waypoint waypoint) { @SuppressLint({"ClickableViewAccessibility", "UseCompatLoadingForDrawables"}) @Override public void onWaypointDetails(Waypoint waypoint, boolean fromList) { + mViews.popupAnchor.setX(0); + mViews.popupAnchor.setY(0); Bundle args = new Bundle(3); args.putBoolean(WaypointInformation.ARG_DETAILS, fromList); if (fromList || mLocationState != LocationState.DISABLED) { @@ -3102,20 +3123,13 @@ public void onRoutesDelete(Set route) { @SuppressLint("UseCompatLoadingForDrawables") @Override public void onFeatureDetails(long id, boolean fromList) { - Bundle args = new Bundle(3); - //args.putBoolean(AmenityInformation.ARG_DETAILS, fromList); + // For some weird reason popupAnchor view limits height of bottom sheet + mViews.popupAnchor.setX(0); + mViews.popupAnchor.setY(0); - if (fromList || mLocationState != LocationState.DISABLED) { - if (mLocationState != LocationState.DISABLED && mLocationService != null) { - Location location = mLocationService.getLocation(); - args.putDouble(AmenityInformation.ARG_LATITUDE, location.getLatitude()); - args.putDouble(AmenityInformation.ARG_LONGITUDE, location.getLongitude()); - } else { - MapPosition position = mMap.getMapPosition(); - args.putDouble(AmenityInformation.ARG_LATITUDE, position.getLatitude()); - args.putDouble(AmenityInformation.ARG_LONGITUDE, position.getLongitude()); - } - } + int language = MapTrekDatabaseHelper.getLanguageId(Configuration.getLanguage()); + Amenity amenity = MapTrekDatabaseHelper.getAmenityData(language, id, mDetailedMapDatabase); + amenityViewModel.setAmenity(amenity); Fragment fragment = mFragmentManager.findFragmentByTag("waypointInformation"); if (fragment != null) { @@ -3125,7 +3139,6 @@ public void onFeatureDetails(long id, boolean fromList) { if (fragment == null) { FragmentFactory factory = mFragmentManager.getFragmentFactory(); fragment = factory.instantiate(getClassLoader(), AmenityInformation.class.getName()); - fragment.setArguments(args); Slide slide = new Slide(Gravity.BOTTOM); // Required to sync with FloatingActionButton slide.setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime)); @@ -3135,8 +3148,6 @@ public void onFeatureDetails(long id, boolean fromList) { ft.addToBackStack("amenityInformation"); ft.commit(); } - ((AmenityInformation) fragment).setPreferredLanguage(Configuration.getLanguage()); - ((AmenityInformation) fragment).setAmenity(id); mViews.extendPanel.setForeground(getDrawable(R.drawable.dim)); mViews.extendPanel.getForeground().setAlpha(0); ObjectAnimator anim = ObjectAnimator.ofInt(mViews.extendPanel.getForeground(), "alpha", 0, 255); @@ -3184,7 +3195,6 @@ private void startMapSelection(boolean zoom) { } FragmentFactory factory = mFragmentManager.getFragmentFactory(); MapSelection fragment = (MapSelection) factory.instantiate(getClassLoader(), MapSelection.class.getName()); - fragment.setMapIndex(mNativeMapIndex); fragment.setEnterTransition(new Slide()); FragmentTransaction ft = mFragmentManager.beginTransaction(); ft.replace(R.id.contentPanel, fragment, "mapSelection"); @@ -3276,7 +3286,7 @@ public void onTransparencyChanged(int transparency) { @Override public void onBeginMapManagement() { - mMapCoverageLayer = new MapCoverageLayer(getApplicationContext(), mMap, mNativeMapIndex, MapTrek.density); + mMapCoverageLayer = new MapCoverageLayer(getApplicationContext(), mMap, mapIndexViewModel.nativeIndex, MapTrek.density); mMap.layers().add(mMapCoverageLayer, MAP_OVERLAYS); MapPosition mapPosition = mMap.getMapPosition(); if (mapPosition.zoomLevel > 8) { @@ -3287,23 +3297,18 @@ public void onBeginMapManagement() { } int[] xy = (int[]) mViews.mapDownloadButton.getTag(R.id.mapKey); if (xy != null) - mNativeMapIndex.selectNativeMap(xy[0], xy[1], Index.ACTION.DOWNLOAD); + mapIndexViewModel.nativeIndex.selectNativeMap(xy[0], xy[1], Index.ACTION.DOWNLOAD); } @Override public void onFinishMapManagement() { mMap.layers().remove(mMapCoverageLayer); mMapCoverageLayer.onDetach(); - mNativeMapIndex.clearSelections(); + mapIndexViewModel.nativeIndex.clearSelections(); mMapCoverageLayer = null; mMap.updateMap(true); } - @Override - public void onManageNativeMaps(boolean hillshadesEnabled) { - mNativeMapIndex.manageNativeMaps(hillshadesEnabled); - } - private void deleteWaypoints(@NonNull Set waypoints) { HashSet sources = new HashSet<>(); for (Waypoint waypoint : waypoints) { @@ -3888,8 +3893,9 @@ public void checkMissingData(MapPosition mapPosition) { if (tileData != null) { int mapX = tile.tileX >> (tile.zoomLevel - 7); int mapY = tile.tileY >> (tile.zoomLevel - 7); - if (!mNativeMapIndex.isDownloading(mapX, mapY) && // Do not show button if this map is already downloading - !(mNativeMapIndex.hasDownloadSizes() && mNativeMapIndex.getNativeMap(mapX, mapY).downloadSize == 0L)) { // Do not show button if there is no map for that area + if (!mapIndexViewModel.nativeIndex.isDownloading(mapX, mapY) && // Do not show button if this map is already downloading + !(mapIndexViewModel.nativeIndex.hasDownloadSizes() + && mapIndexViewModel.nativeIndex.getNativeMap(mapX, mapY).downloadSize == 0L)) { // Do not show button if there is no map for that area visibility = View.VISIBLE; map = new int[]{mapX, mapY}; } @@ -4139,7 +4145,7 @@ public void onReceive(Context context, Intent intent) { mProgressHandler.onProgressStarted(100); WorkManager.getInstance(getApplicationContext()) .getWorkInfoByIdLiveData(id) - .observe(MainActivity.this, (Observer) workInfo -> { + .observe(MainActivity.this, workInfo -> { if (workInfo != null) { Data progress = workInfo.getProgress(); int value = progress.getInt(MapWorker.PROGRESS, 0); @@ -4754,7 +4760,7 @@ public String getStatsString() { Configuration.getTrackingTime() + "," + mWaypointDbDataSource.getWaypointsCount() + "," + mData.size() + "," + - mNativeMapIndex.getMapsCount() + "," + + mapIndexViewModel.nativeIndex.getMapsCount() + "," + mMapIndex.getMaps().size() + "," + Configuration.getFullScreenTimes() + "," + Configuration.getHikingTimes() + "," + diff --git a/app/src/main/java/mobi/maptrek/MapHolder.java b/app/src/main/java/mobi/maptrek/MapHolder.java index 8f8ced25..5fab0fc5 100644 --- a/app/src/main/java/mobi/maptrek/MapHolder.java +++ b/app/src/main/java/mobi/maptrek/MapHolder.java @@ -64,9 +64,5 @@ public interface MapHolder { void setMapLocation(@NonNull GeoPoint point); - void showMarker(@NonNull GeoPoint point, @Nullable String name, boolean amenity); - - void removeMarker(); - void setObjectInteractionEnabled(boolean enabled); } diff --git a/app/src/main/java/mobi/maptrek/fragments/AmenityInformation.java b/app/src/main/java/mobi/maptrek/fragments/AmenityInformation.java index b89b700b..c064edd1 100644 --- a/app/src/main/java/mobi/maptrek/fragments/AmenityInformation.java +++ b/app/src/main/java/mobi/maptrek/fragments/AmenityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Andrey Novikov + * Copyright 2024 Andrey Novikov * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -20,8 +20,6 @@ import android.app.Activity; import android.content.Context; import android.content.res.Resources; -import android.graphics.Rect; -import android.location.Location; import android.net.Uri; import android.os.Bundle; @@ -34,6 +32,7 @@ import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -44,98 +43,76 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.ViewTreeObserver; import android.widget.TextView; import org.oscim.core.GeoPoint; -import java.io.IOException; import java.util.Locale; import mobi.maptrek.Configuration; -import mobi.maptrek.LocationChangeListener; import mobi.maptrek.MapHolder; -import mobi.maptrek.MapTrek; import mobi.maptrek.R; import mobi.maptrek.data.Amenity; import mobi.maptrek.databinding.FragmentAmenityInformationBinding; -import mobi.maptrek.maps.maptrek.MapTrekDatabaseHelper; import mobi.maptrek.util.HelperUtils; import mobi.maptrek.util.ResUtils; import mobi.maptrek.util.StringFormatter; +import mobi.maptrek.viewmodels.AmenityViewModel; +import mobi.maptrek.viewmodels.MapViewModel; -public class AmenityInformation extends Fragment implements LocationChangeListener { - public static final String ARG_LATITUDE = "lat"; - public static final String ARG_LONGITUDE = "lon"; - public static final String ARG_LANG = "lang"; - +public class AmenityInformation extends Fragment { private static final String ALLOWED_URI_CHARS = " @#&=*+-_.,:!?()/~'%"; - private Amenity mAmenity; - private double mLatitude; - private double mLongitude; - private int mLang; - - private FragmentAmenityInformationBinding mViews; private BottomSheetBehavior mBottomSheetBehavior; private AmenityBottomSheetCallback mBottomSheetCallback; private FloatingActionButton mFloatingButton; private FragmentHolder mFragmentHolder; private MapHolder mMapHolder; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } + private AmenityViewModel amenityViewModel; + private FragmentAmenityInformationBinding viewBinding; + private int panelState; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mViews = FragmentAmenityInformationBinding.inflate(inflater, container, false); - final ViewGroup rootView = mViews.getRoot(); - rootView.post(() -> { - updatePeekHeight(rootView, false); - int panelState = BottomSheetBehavior.STATE_COLLAPSED; - if (savedInstanceState != null) { - panelState = savedInstanceState.getInt("panelState", panelState); - View dragHandle = rootView.findViewById(R.id.dragHandle); - dragHandle.setAlpha(panelState == BottomSheetBehavior.STATE_EXPANDED ? 0f : 1f); - } - mBottomSheetBehavior.setState(panelState); - // Workaround for panel partially drawn on first slide - // TODO Try to put transparent view above map - if (Configuration.getHideSystemUI()) - rootView.requestLayout(); - }); - return rootView; + viewBinding = FragmentAmenityInformationBinding.inflate(inflater, container, false); + return viewBinding.getRoot(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - double latitude = Double.NaN; - double longitude = Double.NaN; - - if (savedInstanceState != null) { - latitude = savedInstanceState.getDouble(ARG_LATITUDE); - longitude = savedInstanceState.getDouble(ARG_LONGITUDE); - mLang = savedInstanceState.getInt(ARG_LANG); - } else { - Bundle arguments = getArguments(); - if (arguments != null) { - latitude = arguments.getDouble(ARG_LATITUDE, Double.NaN); - longitude = arguments.getDouble(ARG_LONGITUDE, Double.NaN); + amenityViewModel = new ViewModelProvider(requireActivity()).get(AmenityViewModel.class); + amenityViewModel.getAmenity().observe(getViewLifecycleOwner(), amenity -> { + if (amenity != null) { + updateAmenityInformation(amenity); + requireView().post(this::updatePeekHeight); } - } + }); - final ViewGroup rootView = (ViewGroup) getView(); - assert rootView != null; + MapViewModel mapViewModel = new ViewModelProvider(requireActivity()).get(MapViewModel.class); + mapViewModel.getLocation().observe(getViewLifecycleOwner(), location -> { + if ("unknown".equals(location.getProvider())) { + viewBinding.destination.setVisibility(View.GONE); + } else { + Amenity amenity = amenityViewModel.getAmenity().getValue(); + if (amenity == null) + return; + GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude()); + double dist = point.vincentyDistance(amenity.coordinates); + double bearing = point.bearingTo(amenity.coordinates); + String distance = StringFormatter.distanceH(dist) + " " + StringFormatter.angleH(bearing); + viewBinding.destination.setVisibility(View.VISIBLE); + viewBinding.destination.setTag(true); + viewBinding.destination.setText(distance); + } + }); mFloatingButton = mFragmentHolder.enableActionButton(); mFloatingButton.setImageResource(R.drawable.ic_navigate); mFloatingButton.setOnClickListener(v -> { - mMapHolder.navigateTo(mAmenity.coordinates, mAmenity.name); + Amenity amenity = amenityViewModel.getAmenity().getValue(); + if (amenity != null) + mMapHolder.navigateTo(amenity.coordinates, amenity.name); mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); }); @@ -143,26 +120,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat p.setAnchorId(R.id.bottomSheetPanel); mFloatingButton.setLayoutParams(p); - mMapHolder.showMarker(mAmenity.coordinates, mAmenity.name, true); - updateAmenityInformation(latitude, longitude); - - rootView.findViewById(R.id.dragHandle).setAlpha(1f); mBottomSheetCallback = new AmenityBottomSheetCallback(); - ViewParent parent = rootView.getParent(); - mBottomSheetBehavior = BottomSheetBehavior.from((View) parent); + mBottomSheetBehavior = BottomSheetBehavior.from((View) view.getParent()); mBottomSheetBehavior.addBottomSheetCallback(mBottomSheetCallback); - } - - @Override - public void onResume() { - super.onResume(); - mMapHolder.addLocationChangeListener(this); - } - @Override - public void onPause() { - super.onPause(); - mMapHolder.removeLocationChangeListener(this); + panelState = BottomSheetBehavior.STATE_COLLAPSED; + if (savedInstanceState != null) + panelState = savedInstanceState.getInt("panelState", panelState); + mBottomSheetBehavior.setState(panelState); + viewBinding.dragHandle.setAlpha(panelState == BottomSheetBehavior.STATE_EXPANDED ? 0f : 1f); } @Override @@ -185,7 +151,6 @@ public void onAttach(@NonNull Context context) { public void onDetach() { super.onDetach(); mBackPressedCallback.remove(); - mMapHolder.removeMarker(); mFragmentHolder = null; mMapHolder = null; } @@ -194,211 +159,149 @@ public void onDetach() { public void onDestroyView() { super.onDestroyView(); mBottomSheetBehavior.removeBottomSheetCallback(mBottomSheetCallback); - mViews = null; + viewBinding = null; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putDouble(ARG_LATITUDE, mLatitude); - outState.putDouble(ARG_LONGITUDE, mLongitude); - outState.putInt(ARG_LANG, mLang); - outState.putInt("panelState", mBottomSheetBehavior.getState()); - } - - public void setAmenity(long id) { - try { - mAmenity = MapTrekDatabaseHelper.getAmenityData(mLang, id, MapTrek.getApplication().getDetailedMapDatabase()); - if (isVisible()) { - mMapHolder.showMarker(mAmenity.coordinates, mAmenity.name, true); - updateAmenityInformation(mLatitude, mLongitude); - final ViewGroup rootView = (ViewGroup) getView(); - if (rootView != null) - rootView.post(() -> updatePeekHeight(rootView, true)); - } - } catch (IOException e) { - e.printStackTrace(); - } + outState.putInt("panelState", panelState); } @SuppressLint("ClickableViewAccessibility") - private void updateAmenityInformation(double latitude, double longitude) { + private void updateAmenityInformation(@NonNull Amenity amenity) { final Activity activity = requireActivity(); - boolean hasName = mAmenity.name != null; - String type = mAmenity.type != -1 ? getString(mAmenity.type) : ""; + boolean hasName = amenity.name != null; + String type = amenity.type != -1 ? getString(amenity.type) : ""; - mViews.name.setText(hasName ? mAmenity.name : type); + viewBinding.name.setText(hasName ? amenity.name : type); - if (!hasName || mAmenity.type == -1) { - mViews.type.setVisibility(View.GONE); + if (!hasName || amenity.type == -1) { + viewBinding.type.setVisibility(View.GONE); } else { - mViews.type.setVisibility(View.VISIBLE); - mViews.type.setText(type); + viewBinding.type.setVisibility(View.VISIBLE); + viewBinding.type.setText(type); } - if ("".equals(mAmenity.kind)) { - mViews.kindRow.setVisibility(View.GONE); + if ("".equals(amenity.kind)) { + viewBinding.kindRow.setVisibility(View.GONE); } else { - mViews.kindRow.setVisibility(View.VISIBLE); + viewBinding.kindRow.setVisibility(View.VISIBLE); Resources resources = getResources(); - int id = resources.getIdentifier(mAmenity.kind, "string", activity.getPackageName()); - mViews.kind.setText(resources.getString(id)); + @SuppressLint("DiscouragedApi") + int id = resources.getIdentifier(amenity.kind, "string", activity.getPackageName()); + viewBinding.kind.setText(resources.getString(id)); } - @DrawableRes int icon = ResUtils.getKindIcon(mAmenity.kindNumber); + @DrawableRes int icon = ResUtils.getKindIcon(amenity.kindNumber); if (icon == 0) icon = R.drawable.ic_place; - mViews.kindIcon.setImageResource(icon); + viewBinding.kindIcon.setImageResource(icon); - if (mAmenity.fee == null) { - mViews.feeRow.setVisibility(View.GONE); + if (amenity.fee == null) { + viewBinding.feeRow.setVisibility(View.GONE); } else { - mViews.feeRow.setVisibility(View.VISIBLE); - mViews.fee.setText(R.string.fee); + viewBinding.feeRow.setVisibility(View.VISIBLE); + viewBinding.fee.setText(R.string.fee); } - if (mAmenity.wheelchair == null) { - mViews.wheelchairRow.setVisibility(View.GONE); + if (amenity.wheelchair == null) { + viewBinding.wheelchairRow.setVisibility(View.GONE); } else { - mViews.wheelchairRow.setVisibility(View.VISIBLE); - switch (mAmenity.wheelchair) { + viewBinding.wheelchairRow.setVisibility(View.VISIBLE); + switch (amenity.wheelchair) { case YES: - mViews.wheelchairIcon.setImageResource(R.drawable.ic_accessible); - mViews.wheelchair.setText(R.string.full_access); + viewBinding.wheelchairIcon.setImageResource(R.drawable.ic_accessible); + viewBinding.wheelchair.setText(R.string.full_access); break; case LIMITED: - mViews.wheelchairIcon.setImageResource(R.drawable.ic_accessible); - mViews.wheelchair.setText(R.string.limited_access); + viewBinding.wheelchairIcon.setImageResource(R.drawable.ic_accessible); + viewBinding.wheelchair.setText(R.string.limited_access); break; case NO: - mViews.wheelchairIcon.setImageResource(R.drawable.ic_not_accessible); - mViews.wheelchair.setText(R.string.no_access); + viewBinding.wheelchairIcon.setImageResource(R.drawable.ic_not_accessible); + viewBinding.wheelchair.setText(R.string.no_access); break; } } - if (mAmenity.openingHours == null) { - mViews.openingHoursRow.setVisibility(View.GONE); + if (amenity.openingHours == null) { + viewBinding.openingHoursRow.setVisibility(View.GONE); } else { - mViews.openingHoursRow.setVisibility(View.VISIBLE); - mViews.openingHours.setText(mAmenity.openingHours); + viewBinding.openingHoursRow.setVisibility(View.VISIBLE); + viewBinding.openingHours.setText(amenity.openingHours); } - if (mAmenity.phone == null) { - mViews.phoneRow.setVisibility(View.GONE); + if (amenity.phone == null) { + viewBinding.phoneRow.setVisibility(View.GONE); } else { - mViews.phoneRow.setVisibility(View.VISIBLE); - mViews.phone.setText(PhoneNumberUtils.formatNumber(mAmenity.phone, Locale.getDefault().getCountry())); + viewBinding.phoneRow.setVisibility(View.VISIBLE); + viewBinding.phone.setText(PhoneNumberUtils.formatNumber(amenity.phone, Locale.getDefault().getCountry())); } - if (mAmenity.website == null) { - mViews.websiteRow.setVisibility(View.GONE); + if (amenity.website == null) { + viewBinding.websiteRow.setVisibility(View.GONE); } else { - mViews.websiteRow.setVisibility(View.VISIBLE); - String website = mAmenity.website; + viewBinding.websiteRow.setVisibility(View.VISIBLE); + String website = amenity.website; if (!website.startsWith("http")) website = "http://" + website; String url = website; website = website.replaceFirst("https?://", ""); url = "" + website + ""; - mViews.website.setMovementMethod(LinkMovementMethod.getInstance()); - mViews.website.setText(Html.fromHtml(url)); + viewBinding.website.setMovementMethod(LinkMovementMethod.getInstance()); + viewBinding.website.setText(Html.fromHtml(url)); } - if (mAmenity.wikipedia == null) { - mViews.wikipediaRow.setVisibility(View.GONE); + if (amenity.wikipedia == null) { + viewBinding.wikipediaRow.setVisibility(View.GONE); } else { - mViews.wikipediaRow.setVisibility(View.VISIBLE); - int i = mAmenity.wikipedia.indexOf(':'); + viewBinding.wikipediaRow.setVisibility(View.VISIBLE); + int i = amenity.wikipedia.indexOf(':'); String prefix, text; if (i > 0) { - prefix = mAmenity.wikipedia.substring(0, i) + "."; - text = mAmenity.wikipedia.substring(i + 1); + prefix = amenity.wikipedia.substring(0, i) + "."; + text = amenity.wikipedia.substring(i + 1); } else { prefix = ""; - text = mAmenity.wikipedia; + text = amenity.wikipedia; } String url = "" + text + ""; - mViews.wikipedia.setMovementMethod(LinkMovementMethod.getInstance()); - mViews.wikipedia.setText(Html.fromHtml(url)); + viewBinding.wikipedia.setMovementMethod(LinkMovementMethod.getInstance()); + viewBinding.wikipedia.setText(Html.fromHtml(url)); } - if (Double.isNaN(latitude) || Double.isNaN(longitude)) { - mViews.destination.setVisibility(View.GONE); - } else { - GeoPoint point = new GeoPoint(latitude, longitude); - double dist = point.vincentyDistance(mAmenity.coordinates); - double bearing = point.bearingTo(mAmenity.coordinates); - String distance = StringFormatter.distanceH(dist) + " " + StringFormatter.angleH(bearing); - mViews.destination.setVisibility(View.VISIBLE); - mViews.destination.setTag(true); - mViews.destination.setText(distance); - } - - mViews.coordinates.setText(StringFormatter.coordinates(" ", mAmenity.coordinates.getLatitude(), mAmenity.coordinates.getLongitude())); - - if (HelperUtils.needsTargetedAdvice(Configuration.ADVICE_SWITCH_COORDINATES_FORMAT)) { - // We need this very bulky code to wait until layout is settled and keyboard is completely hidden - // otherwise we get wrong position for advice - final View rootView = mViews.getRoot(); - ViewTreeObserver vto = rootView.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - rootView.postDelayed(() -> { - if (isVisible()) { - Rect r = new Rect(); - mViews.coordinates.getGlobalVisibleRect(r); - HelperUtils.showTargetedAdvice(activity, Configuration.ADVICE_SWITCH_COORDINATES_FORMAT, R.string.advice_switch_coordinates_format, r); - } - }, 1000); - } - }); - } - - mViews.coordinates.setOnTouchListener((v, event) -> { + viewBinding.coordinates.setText(StringFormatter.coordinates(" ", amenity.coordinates.getLatitude(), amenity.coordinates.getLongitude())); + viewBinding.coordinates.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_UP) { - if (event.getX() >= mViews.coordinates.getRight() - mViews.coordinates.getTotalPaddingRight()) { - mMapHolder.shareLocation(mAmenity.coordinates, mAmenity.name); + if (event.getX() >= viewBinding.coordinates.getRight() - viewBinding.coordinates.getTotalPaddingRight()) { + mMapHolder.shareLocation(amenity.coordinates, amenity.name); } else { StringFormatter.coordinateFormat++; if (StringFormatter.coordinateFormat == 5) StringFormatter.coordinateFormat = 0; - mViews.coordinates.setText(StringFormatter.coordinates(" ", mAmenity.coordinates.getLatitude(), mAmenity.coordinates.getLongitude())); + viewBinding.coordinates.setText(StringFormatter.coordinates(" ", amenity.coordinates.getLatitude(), amenity.coordinates.getLongitude())); Configuration.setCoordinatesFormat(StringFormatter.coordinateFormat); } } return true; }); - if (mAmenity.altitude != Integer.MIN_VALUE) { - mViews.elevation.setText(getString(R.string.place_altitude, StringFormatter.elevationH(mAmenity.altitude))); - mViews.elevation.setVisibility(View.VISIBLE); + if (amenity.altitude != Integer.MIN_VALUE) { + viewBinding.elevation.setText(getString(R.string.place_altitude, StringFormatter.elevationH(amenity.altitude))); + viewBinding.elevation.setVisibility(View.VISIBLE); } else { - mViews.elevation.setVisibility(View.GONE); + viewBinding.elevation.setVisibility(View.GONE); } - - mLatitude = latitude; - mLongitude = longitude; } - private void updatePeekHeight(ViewGroup rootView, boolean setState) { - View dragHandle = rootView.findViewById(R.id.dragHandle); - View nameView = rootView.findViewById(R.id.name); - View typeView = rootView.findViewById(R.id.type); - mBottomSheetBehavior.setPeekHeight(dragHandle.getHeight() * 2 + nameView.getHeight() + typeView.getHeight()); - if (setState) - mBottomSheetBehavior.setState(mBottomSheetBehavior.getState()); - } - - @Override - public void onLocationChanged(Location location) { - updateAmenityInformation(location.getLatitude(), location.getLongitude()); - } - - public void setPreferredLanguage(String lang) { - mLang = MapTrekDatabaseHelper.getLanguageId(lang); + private void updatePeekHeight() { + int height = viewBinding.dragHandle.getHeight() * 2 + viewBinding.name.getHeight(); + if (viewBinding.type.getVisibility() == View.VISIBLE) + height += viewBinding.type.getHeight(); + mBottomSheetBehavior.setPeekHeight(height); + // Somehow setPeekHeight breaks state on first show + mBottomSheetBehavior.setState(panelState); } OnBackPressedCallback mBackPressedCallback = new OnBackPressedCallback(true) { @@ -412,6 +315,7 @@ private class AmenityBottomSheetCallback extends BottomSheetBehavior.BottomSheet @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { + amenityViewModel.setAmenity(null); mBottomSheetBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO); mFragmentHolder.disableActionButton(); CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) mFloatingButton.getLayoutParams(); @@ -420,9 +324,13 @@ public void onStateChanged(@NonNull View bottomSheet, int newState) { mFloatingButton.setAlpha(1f); mFragmentHolder.popCurrent(); } + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + panelState = newState; + } if (newState == BottomSheetBehavior.STATE_EXPANDED) { - TextView coordsView = bottomSheet.findViewById(R.id.coordinates); - HelperUtils.showTargetedAdvice(getActivity(), Configuration.ADVICE_SWITCH_COORDINATES_FORMAT, R.string.advice_switch_coordinates_format, coordsView, true); + panelState = newState; + TextView view = bottomSheet.findViewById(R.id.coordinates); + HelperUtils.showTargetedAdvice(getActivity(), Configuration.ADVICE_SWITCH_COORDINATES_FORMAT, R.string.advice_switch_coordinates_format, view, true); } } diff --git a/app/src/main/java/mobi/maptrek/fragments/BaseMapDownload.java b/app/src/main/java/mobi/maptrek/fragments/BaseMapDownload.java index 7e233421..e4270103 100644 --- a/app/src/main/java/mobi/maptrek/fragments/BaseMapDownload.java +++ b/app/src/main/java/mobi/maptrek/fragments/BaseMapDownload.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Andrey Novikov + * Copyright 2024 Andrey Novikov * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -28,30 +28,25 @@ import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; import mobi.maptrek.R; -import mobi.maptrek.maps.maptrek.Index; +import mobi.maptrek.viewmodels.MapIndexViewModel; public class BaseMapDownload extends DialogFragment { - @NonNull - private final Index mMapIndex; - - public BaseMapDownload(@NonNull Index mapIndex) { - super(); - mMapIndex = mapIndex; - } - @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final View dialogView = getLayoutInflater().inflate(R.layout.fragment_basemap_download, null); + MapIndexViewModel mapIndexViewModel = new ViewModelProvider(requireActivity()).get(MapIndexViewModel.class); + TextView messageView = dialogView.findViewById(R.id.message); - long size = mMapIndex.getBaseMapSize(); + long size = mapIndexViewModel.nativeIndex.getBaseMapSize(); messageView.setText(getString(R.string.msgBaseMapDownload, Formatter.formatFileSize(getContext(), size))); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()); - dialogBuilder.setPositiveButton(R.string.actionDownload, (dialog, which) -> mMapIndex.downloadBaseMap()); + dialogBuilder.setPositiveButton(R.string.actionDownload, (dialog, which) -> mapIndexViewModel.nativeIndex.downloadBaseMap()); dialogBuilder.setNegativeButton(R.string.actionSkip, (dialog, which) -> {}); dialogBuilder.setView(dialogView); diff --git a/app/src/main/java/mobi/maptrek/fragments/MapSelection.java b/app/src/main/java/mobi/maptrek/fragments/MapSelection.java index 35992966..65549161 100644 --- a/app/src/main/java/mobi/maptrek/fragments/MapSelection.java +++ b/app/src/main/java/mobi/maptrek/fragments/MapSelection.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Andrey Novikov + * Copyright 2024 Andrey Novikov * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -21,7 +21,6 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; -import android.os.AsyncTask; import android.os.Bundle; import com.google.android.material.floatingactionbutton.FloatingActionButton; import android.text.format.Formatter; @@ -29,96 +28,104 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.CheckBox; -import android.widget.ImageButton; -import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; - import mobi.maptrek.Configuration; import mobi.maptrek.R; +import mobi.maptrek.databinding.FragmentMapSelectionBinding; import mobi.maptrek.maps.maptrek.Index; import mobi.maptrek.util.HelperUtils; +import mobi.maptrek.viewmodels.MapIndexViewModel; -public class MapSelection extends Fragment implements Index.MapStateListener { +public class MapSelection extends Fragment { private static final Logger logger = LoggerFactory.getLogger(MapSelection.class); - private static final long INDEX_CACHE_TIMEOUT = 24 * 3600 * 1000L; // One day - private static final long INDEX_CACHE_EXPIRATION = 60 * 24 * 3600 * 1000L; // Two months - private static final long HILLSHADE_CACHE_TIMEOUT = 60 * 24 * 3600 * 1000L; // Two months - private OnMapActionListener mListener; private FragmentHolder mFragmentHolder; private FloatingActionButton mFloatingButton; - private Index mMapIndex; - private View mDownloadCheckboxHolder; - private View mHillshadesCheckboxHolder; - private CheckBox mDownloadBasemap; - private CheckBox mDownloadHillshades; - private TextView mMessageView; - private TextView mStatusView; - private TextView mCounterView; - private ImageButton mHelpButton; - private Resources mResources; - private boolean mIsDownloadingIndex; - private File mCacheFile; - private File mHillshadeCacheFile; - private int mCounter; + private Resources resources; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); + private MapIndexViewModel mapIndexViewModel; + private FragmentMapSelectionBinding viewBinding; + private String statsString; + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + viewBinding = FragmentMapSelectionBinding.inflate(inflater, container, false); + return viewBinding.getRoot(); } @Override - public void onResume() { - super.onResume(); - updateUI(mMapIndex.getMapStats()); - if (!mMapIndex.hasDownloadSizes() && mCacheFile.exists()) { - mIsDownloadingIndex = true; - new LoadMapIndex().execute(); - } - } + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mapIndexViewModel = new ViewModelProvider(requireActivity()).get(MapIndexViewModel.class); + mapIndexViewModel.getNativeIndexState().observe(getViewLifecycleOwner(), indexStats -> { + int count = indexStats.download + indexStats.remove; + logger.debug("Selected maps count: {}", count); + viewBinding.message.setText(resources.getQuantityString(R.plurals.itemsSelected, count, count)); + if (indexStats.downloadSize > 0L) { + viewBinding.status.setVisibility(View.VISIBLE); + viewBinding.status.setText(getString(R.string.msgDownloadSize, Formatter.formatFileSize(getContext(), indexStats.downloadSize))); + } else { + viewBinding.status.setVisibility(View.GONE); + } + updateUI(indexStats); + }); + mapIndexViewModel.getBaseMapState().observe(getViewLifecycleOwner(), baseMapState -> { + if (baseMapState.outdated || baseMapState.version == 0) { + @StringRes int msgId = baseMapState.version > 0 ? R.string.downloadUpdatedBasemap : R.string.downloadBasemap; + viewBinding.downloadBasemap.setText(getString(msgId, Formatter.formatFileSize(getContext(), baseMapState.size))); + viewBinding.downloadCheckboxHolder.setVisibility(View.VISIBLE); + } else { + viewBinding.downloadCheckboxHolder.setVisibility(View.GONE); + } + }); + mapIndexViewModel.getActionState().observe(getViewLifecycleOwner(), actionState -> { + if (actionState == null) + return; + if (actionState.action == Index.ACTION.CANCEL) { + final Activity activity = getActivity(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.msgCancelDownload); + builder.setPositiveButton(R.string.yes, (dialog, which) -> mapIndexViewModel.nativeIndex.cancelDownload(actionState.x, actionState.y)); + builder.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()); + AlertDialog dialog = builder.create(); + dialog.show(); + } else if (actionState.action == Index.ACTION.DOWNLOAD) { + mapIndexViewModel.loadMapIndexes(statsString); + } + }); + mapIndexViewModel.getIndexDownloadProgressState().observe(getViewLifecycleOwner(), progress -> { + viewBinding.progress.setVisibility(progress == -1 ? View.GONE : View.VISIBLE); + if (progress == -2) { // error + viewBinding.progress.setText(R.string.msgIndexDownloadFailed); + } else if (progress == 0) { + viewBinding.progress.setText(R.string.msgEstimateDownloadSize); + } else { + viewBinding.progress.setText(getString(R.string.msgEstimateDownloadSizePlaceholder, getString(R.string.msgEstimateDownloadSize), progress)); + } + }); - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.fragment_map_selection, container, false); - mHillshadesCheckboxHolder = rootView.findViewById(R.id.hillshadesCheckboxHolder); - mDownloadHillshades = rootView.findViewById(R.id.downloadHillshades); - mDownloadHillshades.setChecked(Configuration.getHillshadesEnabled()); - mDownloadHillshades.setOnCheckedChangeListener((buttonView, isChecked) -> { - mMapIndex.accountHillshades(isChecked); - updateUI(mMapIndex.getMapStats()); + viewBinding.downloadHillshades.setChecked(Configuration.getHillshadesEnabled()); + //noinspection CodeBlock2Expr + viewBinding.downloadHillshades.setOnCheckedChangeListener((buttonView, isChecked) -> { + mapIndexViewModel.nativeIndex.accountHillshades(isChecked); + }); + //noinspection CodeBlock2Expr + viewBinding.downloadBasemap.setOnCheckedChangeListener((buttonView, isChecked) -> { + updateUI(mapIndexViewModel.nativeIndex.getMapStats()); }); - mDownloadCheckboxHolder = rootView.findViewById(R.id.downloadCheckboxHolder); - mDownloadBasemap = rootView.findViewById(R.id.downloadBasemap); - mDownloadBasemap.setOnCheckedChangeListener((buttonView, isChecked) -> updateUI(mMapIndex.getMapStats())); - mMessageView = rootView.findViewById(R.id.message); - mMessageView.setText(mResources.getQuantityString(R.plurals.itemsSelected, 0, 0)); - mStatusView = rootView.findViewById(R.id.status); - mCounterView = rootView.findViewById(R.id.count); - mHelpButton = rootView.findViewById(R.id.helpButton); - mHelpButton.setOnClickListener(v -> { + viewBinding.message.setText(resources.getQuantityString(R.plurals.itemsSelected, 0, 0)); + viewBinding.helpButton.setOnClickListener(v -> { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.msgMapSelectionExplanation); builder.setPositiveButton(R.string.ok, null); @@ -127,14 +134,15 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa }); if (HelperUtils.needsTargetedAdvice(Configuration.ADVICE_ACTIVE_MAPS_SIZE)) { - ViewTreeObserver vto = rootView.getViewTreeObserver(); + ViewTreeObserver vto = view.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - if (mMapIndex.getMapDatabaseSize() > (1L << 32)) { // 4 GB + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (mapIndexViewModel.nativeIndex.getMapDatabaseSize() > (1L << 32)) { // 4 GB Rect r = new Rect(); - mCounterView.getGlobalVisibleRect(r); + if (!viewBinding.count.getGlobalVisibleRect(r)) + return; r.left = r.right - r.width() / 3; // focus on size HelperUtils.showTargetedAdvice(getActivity(), Configuration.ADVICE_ACTIVE_MAPS_SIZE, R.string.advice_active_maps_size, r); } @@ -142,28 +150,19 @@ public void onGlobalLayout() { }); } - return rootView; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mListener.onBeginMapManagement(); mFloatingButton = mFragmentHolder.enableActionButton(); mFloatingButton.setImageResource(R.drawable.ic_file_download); mFloatingButton.setOnClickListener(v -> { - if (mDownloadBasemap.isChecked()) { - mMapIndex.downloadBaseMap(); - } - if (mCounter > 0) { - mListener.onManageNativeMaps(mDownloadHillshades.isChecked()); + if (viewBinding.downloadBasemap.isChecked()) { + mapIndexViewModel.nativeIndex.downloadBaseMap(); } - if (mDownloadBasemap.isChecked() || mCounter > 0) { - mListener.onFinishMapManagement(); + Index.IndexStats indexStats = mapIndexViewModel.getNativeIndexState().getValue(); + if (indexStats != null && indexStats.download + indexStats.remove > 0) { + mapIndexViewModel.nativeIndex.manageNativeMaps(viewBinding.downloadHillshades.isChecked()); } - mFragmentHolder.disableActionButton(); + mListener.onFinishMapManagement(); mFragmentHolder.popCurrent(); }); } @@ -181,293 +180,77 @@ public void onAttach(@NonNull Context context) { } catch (ClassCastException e) { throw new ClassCastException(context + " must implement FragmentHolder"); } - mResources = getResources(); - - File cacheDir = context.getExternalCacheDir(); - mCacheFile = new File(cacheDir, "mapIndex"); - mHillshadeCacheFile = new File(cacheDir, "hillshadeIndex"); - - mMapIndex.addMapStateListener(this); - requireActivity().getOnBackPressedDispatcher().addCallback(this, mBackPressedCallback); + resources = getResources(); + requireActivity().getOnBackPressedDispatcher().addCallback(this, backPressedCallback); } @Override public void onDetach() { super.onDetach(); - mBackPressedCallback.remove(); - mMapIndex.removeMapStateListener(this); + backPressedCallback.remove(); mFragmentHolder = null; mListener = null; - mResources = null; + resources = null; + } + + @Override + public void onResume() { + super.onResume(); + statsString = mFragmentHolder.getStatsString(); + if (mapIndexViewModel.cacheFile.exists()) { + mapIndexViewModel.loadMapIndexes(statsString); + } } @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); + public void onDestroyView() { + super.onDestroyView(); + mFragmentHolder.disableActionButton(); } - OnBackPressedCallback mBackPressedCallback = new OnBackPressedCallback(true) { + OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { - mFragmentHolder.disableActionButton(); mListener.onFinishMapManagement(); this.remove(); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; - @Override - public void onMapSelected(final int x, final int y, Index.ACTION action, Index.IndexStats stats) { - if (action == Index.ACTION.CANCEL) { - final Activity activity = getActivity(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.msgCancelDownload); - builder.setPositiveButton(R.string.yes, (dialog, which) -> mMapIndex.cancelDownload(x, y)); - builder.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()); - AlertDialog dialog = builder.create(); - dialog.show(); - return; - } - updateUI(stats); - if (action == Index.ACTION.DOWNLOAD && !mMapIndex.hasDownloadSizes() && !mIsDownloadingIndex) { - mIsDownloadingIndex = true; - new LoadMapIndex().execute(); - } - } - - public void setMapIndex(Index mapIndex) { - mMapIndex = mapIndex; - mMapIndex.accountHillshades(Configuration.getHillshadesEnabled()); - } - private void updateUI(Index.IndexStats stats) { - if (!isVisible()) - return; - - if (mMapIndex.isBaseMapOutdated() || mMapIndex.getBaseMapVersion() == 0) { - @StringRes int msgId = mMapIndex.getBaseMapVersion() > 0 ? R.string.downloadUpdatedBasemap : R.string.downloadBasemap; - mDownloadBasemap.setText(getString(msgId, Formatter.formatFileSize(getContext(), mMapIndex.getBaseMapSize()))); - mDownloadCheckboxHolder.setVisibility(View.VISIBLE); - } - - mCounter = stats.download + stats.remove; - mMessageView.setText(mResources.getQuantityString(R.plurals.itemsSelected, mCounter, mCounter)); - // can be null when fragment is not yet visible - if (mFloatingButton != null) { - if (mDownloadBasemap.isChecked() || stats.download > 0) { - mFloatingButton.setImageResource(R.drawable.ic_file_download); - ((View)mFloatingButton).setVisibility(View.VISIBLE); - mHelpButton.setVisibility(View.INVISIBLE); - if (stats.download > 0) - mHillshadesCheckboxHolder.setVisibility(View.VISIBLE); - } else if (stats.remove > 0) { - mFloatingButton.setImageResource(R.drawable.ic_delete); - ((View)mFloatingButton).setVisibility(View.VISIBLE); - mHelpButton.setVisibility(View.INVISIBLE); - mHillshadesCheckboxHolder.setVisibility(View.GONE); - } else { - ((View)mFloatingButton).setVisibility(View.GONE); - mHelpButton.setVisibility(View.VISIBLE); - mHillshadesCheckboxHolder.setVisibility(View.GONE); - } - } - if (stats.downloadSize > 0L) { - mStatusView.setVisibility(View.VISIBLE); - mStatusView.setText(getString(R.string.msgDownloadSize, Formatter.formatFileSize(getContext(), stats.downloadSize))); - } else if (!mIsDownloadingIndex) { - mStatusView.setVisibility(View.GONE); + if (viewBinding.downloadBasemap.isChecked() || stats.download > 0) { + mFloatingButton.setImageResource(R.drawable.ic_file_download); + mFloatingButton.setVisibility(View.VISIBLE); + viewBinding.helpButton.setVisibility(View.INVISIBLE); + if (stats.download > 0) + viewBinding.hillshadesCheckboxHolder.setVisibility(View.VISIBLE); + } else if (stats.remove > 0) { + mFloatingButton.setImageResource(R.drawable.ic_delete); + mFloatingButton.setVisibility(View.VISIBLE); + viewBinding.helpButton.setVisibility(View.INVISIBLE); + viewBinding.hillshadesCheckboxHolder.setVisibility(View.GONE); + } else { + mFloatingButton.setVisibility(View.GONE); + viewBinding.helpButton.setVisibility(View.VISIBLE); + viewBinding.hillshadesCheckboxHolder.setVisibility(View.GONE); } StringBuilder stringBuilder = new StringBuilder(); if (stats.loaded > 0) { - stringBuilder.append(mResources.getQuantityString(R.plurals.loadedAreas, stats.loaded, stats.loaded)); + stringBuilder.append(resources.getQuantityString(R.plurals.loadedAreas, stats.loaded, stats.loaded)); stringBuilder.append(" ("); - stringBuilder.append(Formatter.formatFileSize(getContext(), mMapIndex.getMapDatabaseSize())); + stringBuilder.append(Formatter.formatFileSize(getContext(), mapIndexViewModel.nativeIndex.getMapDatabaseSize())); stringBuilder.append(")"); } if (stats.downloading > 0) { if (stringBuilder.length() > 0) stringBuilder.append(", "); - stringBuilder.append(mResources.getQuantityString(R.plurals.downloading, stats.downloading, stats.downloading)); + stringBuilder.append(resources.getQuantityString(R.plurals.downloading, stats.downloading, stats.downloading)); } if (stringBuilder.length() > 0) { - mCounterView.setVisibility(View.VISIBLE); - mCounterView.setText(stringBuilder); + viewBinding.count.setVisibility(View.VISIBLE); + viewBinding.count.setText(stringBuilder); } else { - mCounterView.setVisibility(View.GONE); - } - } - - @Override - public void onHasDownloadSizes() { - } - - @Override - public void onStatsChanged() { - requireActivity().runOnUiThread(() -> updateUI(mMapIndex.getMapStats())); - } - - @Override - public void onHillshadeAccountingChanged(boolean account) { - } - - private class LoadMapIndex extends AsyncTask { - - private int mProgress; - private int mDivider; - - @Override - protected void onPreExecute() { - mStatusView.setVisibility(View.VISIBLE); - mStatusView.setText(R.string.msgEstimateDownloadSize); - mProgress = 0; - } - - @Override - protected Boolean doInBackground(Void... params) { - long now = System.currentTimeMillis(); - boolean validCache = mCacheFile.lastModified() + INDEX_CACHE_TIMEOUT > now; - boolean validHillshadeCache = mHillshadeCacheFile.lastModified() + HILLSHADE_CACHE_TIMEOUT > now; - mDivider = validHillshadeCache ? 1 : 2; - // load map index - try { - boolean loaded = false; - InputStream in; - if (!validCache) { - URL url = new URL(Index.getIndexUri().toString() + "?" + mFragmentHolder.getStatsString()); - HttpURLConnection urlConnection = null; - try { - urlConnection = (HttpURLConnection) url.openConnection(); - in = urlConnection.getInputStream(); - File tmpFile = new File(mCacheFile.getAbsoluteFile() + "_tmp"); - OutputStream out = new FileOutputStream(tmpFile); - loadMapIndex(in, out); - loaded = tmpFile.renameTo(mCacheFile); - } catch (IOException e) { - logger.error("Failed to download map index", e); - } finally { - if (urlConnection != null) - urlConnection.disconnect(); - } - } - if (!loaded) { - in = new FileInputStream(mCacheFile); - loadMapIndex(in, null); - } - } catch (Exception e) { - logger.error("Failed to load map index", e); - // remove cache on any error - //noinspection ResultOfMethodCallIgnored - mCacheFile.delete(); - return false; - } - // load hillshade index - try { - boolean loaded = false; - InputStream in; - if (!validHillshadeCache) { - URL url = new URL(Index.getHillshadeIndexUri().toString()); - HttpURLConnection urlConnection = null; - try { - urlConnection = (HttpURLConnection) url.openConnection(); - in = urlConnection.getInputStream(); - File tmpFile = new File(mHillshadeCacheFile.getAbsoluteFile() + "_tmp"); - OutputStream out = new FileOutputStream(mHillshadeCacheFile); - loadHillshadesIndex(in, out); - loaded = tmpFile.renameTo(mHillshadeCacheFile); - } catch (IOException e) { - logger.error("Failed to download hillshades index", e); - } finally { - if (urlConnection != null) - urlConnection.disconnect(); - } - } - if (!loaded) { - in = new FileInputStream(mHillshadeCacheFile); - loadHillshadesIndex(in, null); - } - } catch (Exception e) { - logger.error("Failed to load hillshades index", e); - // remove cache on any error - //noinspection ResultOfMethodCallIgnored - mHillshadeCacheFile.delete(); - return false; - } - return true; - } - - private void loadMapIndex(InputStream in, OutputStream out) throws IOException { - DataInputStream data = new DataInputStream(new BufferedInputStream(in)); - DataOutputStream dataOut = null; - if (out != null) - dataOut = new DataOutputStream(new BufferedOutputStream(out)); - - for (int x = 0; x < 128; x++) - for (int y = 0; y < 128; y++) { - short date = data.readShort(); - int size = data.readInt(); - if (dataOut != null) { - dataOut.writeShort(date); - dataOut.writeInt(size); - } - mMapIndex.setNativeMapStatus(x, y, date, size); - int p = (int) ((x * 128 + y) / 163.84 / mDivider); - if (p > mProgress) { - mProgress = p; - publishProgress(mProgress); - } - } - short date = data.readShort(); - int size = data.readInt(); - mMapIndex.setBaseMapStatus(date, size); - if (dataOut != null) { - dataOut.writeShort(date); - dataOut.writeInt(size); - dataOut.close(); - } - } - - private void loadHillshadesIndex(InputStream in, OutputStream out) throws IOException { - DataInputStream data = new DataInputStream(new BufferedInputStream(in)); - DataOutputStream dataOut = null; - if (out != null) - dataOut = new DataOutputStream(new BufferedOutputStream(out)); - - for (int x = 0; x < 128; x++) - for (int y = 0; y < 128; y++) { - byte version = data.readByte(); - int size = data.readInt(); - if (dataOut != null) { - dataOut.writeByte(version); - dataOut.writeInt(size); - } - mMapIndex.setHillshadeStatus(x, y, version, size); - int p = (int) ((x * 128 + y) / 163.84 / mDivider); - if (p > mProgress) { - mProgress = p; - publishProgress(mProgress); - } - } - if (dataOut != null) { - dataOut.close(); - } - } - - @Override - protected void onPostExecute(Boolean result) { - mIsDownloadingIndex = false; - if (result) { - boolean expired = mCacheFile.lastModified() + INDEX_CACHE_EXPIRATION < System.currentTimeMillis(); - mMapIndex.setHasDownloadSizes(expired); - updateUI(mMapIndex.getMapStats()); - } else { - mStatusView.setText(R.string.msgIndexDownloadFailed); - } - } - - @Override - protected void onProgressUpdate(Integer... values) { - if (isVisible()) - mStatusView.setText(getString(R.string.msgEstimateDownloadSizePlaceholder, getString(R.string.msgEstimateDownloadSize), values[0])); + viewBinding.count.setVisibility(View.GONE); } } } diff --git a/app/src/main/java/mobi/maptrek/fragments/MarkerInformation.java b/app/src/main/java/mobi/maptrek/fragments/MarkerInformation.java index 048e074a..106a4959 100644 --- a/app/src/main/java/mobi/maptrek/fragments/MarkerInformation.java +++ b/app/src/main/java/mobi/maptrek/fragments/MarkerInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Andrey Novikov + * Copyright 2024 Andrey Novikov * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -19,86 +19,58 @@ import android.content.Context; import android.os.Bundle; -import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.floatingactionbutton.FloatingActionButton; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; - -import org.oscim.core.GeoPoint; +import androidx.lifecycle.ViewModelProvider; import mobi.maptrek.Configuration; -import mobi.maptrek.MapHolder; import mobi.maptrek.R; +import mobi.maptrek.databinding.FragmentMarkerInformationBinding; import mobi.maptrek.util.StringFormatter; +import mobi.maptrek.viewmodels.MapViewModel; public class MarkerInformation extends Fragment { - public static final String ARG_LATITUDE = "latitude"; - public static final String ARG_LONGITUDE = "longitude"; - public static final String ARG_NAME = "name"; - - private double mLatitude; - private double mLongitude; - private String mName; - private OnWaypointActionListener mListener; private FragmentHolder mFragmentHolder; - private MapHolder mMapHolder; + private MapViewModel mapViewModel; + private FragmentMarkerInformationBinding viewBinding; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_marker_information, container, false); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + viewBinding = FragmentMarkerInformationBinding.inflate(inflater, container, false); + return viewBinding.getRoot(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - - if (savedInstanceState != null) { - mLatitude = savedInstanceState.getDouble(ARG_LATITUDE); - mLongitude = savedInstanceState.getDouble(ARG_LONGITUDE); - mName = savedInstanceState.getString(ARG_NAME); - } else { - Bundle arguments = getArguments(); - if (arguments != null) { - mLatitude = getArguments().getDouble(ARG_LATITUDE); - mLongitude = getArguments().getDouble(ARG_LONGITUDE); - mName = getArguments().getString(ARG_NAME); + mapViewModel = new ViewModelProvider(requireActivity()).get(MapViewModel.class); + mapViewModel.getMarkerState().observe(requireActivity(), markerState -> { + if (markerState.isShown()) { + String name = markerState.getName(); + if (name == null || "".equals(name)) + name = StringFormatter.coordinates(markerState.getCoordinates()); + viewBinding.name.setText(name); } - } - - String name; - if (mName != null && !"".equals(mName)) - name = mName; - else - name = StringFormatter.coordinates(" ", mLatitude, mLongitude); - //noinspection ConstantConditions - ((TextView) getView().findViewById(R.id.name)).setText(name); - - final GeoPoint point = new GeoPoint(mLatitude, mLongitude); - mMapHolder.showMarker(point, name, false); + }); FloatingActionButton floatingButton = mFragmentHolder.enableActionButton(); floatingButton.setImageDrawable(AppCompatResources.getDrawable(view.getContext(), R.drawable.ic_pin_drop)); floatingButton.setOnClickListener(v -> { - String name1; - if (mName != null && !"".equals(mName)) - name1 = mName; - else - name1 = getString(R.string.place_name, Configuration.getPointsCounter()); - mListener.onWaypointCreate(point, name1, true, true); + MapViewModel.MarkerState markerState = mapViewModel.getMarkerState().getValue(); + if (markerState == null) + return; + String name = markerState.getName(); + if (name == null || "".equals(name)) + name = getString(R.string.place_name, Configuration.getPointsCounter()); + mListener.onWaypointCreate(markerState.getCoordinates(), name, true, true); mFragmentHolder.disableActionButton(); mFragmentHolder.popCurrent(); }); @@ -112,11 +84,6 @@ public void onAttach(@NonNull Context context) { } catch (ClassCastException e) { throw new ClassCastException(context + " must implement OnWaypointActionListener"); } - try { - mMapHolder = (MapHolder) context; - } catch (ClassCastException e) { - throw new ClassCastException(context + " must implement MapHolder"); - } try { mFragmentHolder = (FragmentHolder) context; } catch (ClassCastException e) { @@ -129,25 +96,16 @@ public void onAttach(@NonNull Context context) { public void onDetach() { super.onDetach(); mBackPressedCallback.remove(); - mMapHolder.removeMarker(); mFragmentHolder = null; mListener = null; - mMapHolder = null; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putDouble(ARG_LATITUDE, mLatitude); - outState.putDouble(ARG_LONGITUDE, mLongitude); - outState.putString(ARG_NAME, mName); } OnBackPressedCallback mBackPressedCallback = new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { + mapViewModel.removeMarker(); mFragmentHolder.disableActionButton(); - this.remove(); + remove(); requireActivity().getOnBackPressedDispatcher().onBackPressed(); } }; diff --git a/app/src/main/java/mobi/maptrek/fragments/OnMapActionListener.java b/app/src/main/java/mobi/maptrek/fragments/OnMapActionListener.java index 1c3abaf5..01b4fd95 100644 --- a/app/src/main/java/mobi/maptrek/fragments/OnMapActionListener.java +++ b/app/src/main/java/mobi/maptrek/fragments/OnMapActionListener.java @@ -27,5 +27,4 @@ public interface OnMapActionListener { void onTransparencyChanged(int transparency); void onBeginMapManagement(); void onFinishMapManagement(); - void onManageNativeMaps(boolean hillshadesEnabled); } diff --git a/app/src/main/java/mobi/maptrek/fragments/Ruler.java b/app/src/main/java/mobi/maptrek/fragments/Ruler.java index d668c62c..5191a7a8 100644 --- a/app/src/main/java/mobi/maptrek/fragments/Ruler.java +++ b/app/src/main/java/mobi/maptrek/fragments/Ruler.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Andrey Novikov + * Copyright 2024 Andrey Novikov * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -21,11 +21,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; import org.oscim.android.canvas.AndroidBitmap; import org.oscim.backend.canvas.Bitmap; @@ -40,6 +40,7 @@ import mobi.maptrek.MapTrek; import mobi.maptrek.R; import mobi.maptrek.data.Route; +import mobi.maptrek.databinding.FragmentRulerBinding; import mobi.maptrek.layers.RouteLayer; import mobi.maptrek.layers.marker.ItemizedLayer; import mobi.maptrek.layers.marker.MarkerItem; @@ -50,83 +51,75 @@ public class Ruler extends Fragment implements ItemizedLayer.OnItemGestureListener { private MapHolder mMapHolder; - private View mMeasurementsView; - private TextView mDistanceView; - private TextView mSizeView; - private MapPosition mMapPosition; private RouteLayer mRouteLayer; private ItemizedLayer mPointLayer; - private Stack mPointHistory; - private Route mRoute; + private RulerViewModel viewModel; + private FragmentRulerBinding viewBinding; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setRetainInstance(true); - mRoute = new Route(); mMapPosition = new MapPosition(); - mPointHistory = new Stack<>(); + viewModel = new ViewModelProvider(this).get(RulerViewModel.class); } - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_ruler, container, false); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + viewBinding = FragmentRulerBinding.inflate(inflater, container, false); + return viewBinding.getRoot(); + } - mMeasurementsView = rootView.findViewById(R.id.measurements); - mDistanceView = rootView.findViewById(R.id.distance); - mSizeView = rootView.findViewById(R.id.size); + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - ImageButton addButton = rootView.findViewById(R.id.addButton); - addButton.setOnClickListener(v -> { + viewBinding.addButton.setOnClickListener(v -> { if (mMapHolder.getMap().getMapPosition(mMapPosition)) { GeoPoint point = mMapPosition.getGeoPoint(); - mRoute.addInstruction(point); + viewModel.route.addInstruction(point); MarkerItem marker = new MarkerItem(point, null, null, point); mPointLayer.addItem(marker); - mPointHistory.push(point); + viewModel.pointHistory.push(point); mMapHolder.getMap().updateMap(true); updateTrackMeasurements(); } }); - ImageButton insertButton = rootView.findViewById(R.id.insertButton); - insertButton.setOnClickListener(v -> { + viewBinding.insertButton.setOnClickListener(v -> { if (mMapHolder.getMap().getMapPosition(mMapPosition)) { GeoPoint point = mMapPosition.getGeoPoint(); - mRoute.insertInstruction(point); + viewModel.route.insertInstruction(point); MarkerItem marker = new MarkerItem(point, null, null, point); mPointLayer.addItem(marker); - mPointHistory.push(point); + viewModel.pointHistory.push(point); mMapHolder.getMap().updateMap(true); updateTrackMeasurements(); } }); - ImageButton removeButton = rootView.findViewById(R.id.removeButton); - removeButton.setOnClickListener(v -> { - if (mRoute.length() > 0) { + viewBinding.removeButton.setOnClickListener(v -> { + if (viewModel.route.length() > 0) { mMapHolder.getMap().getMapPosition(mMapPosition); - Route.Instruction instruction = mRoute.getNearestInstruction(mMapPosition.getGeoPoint()); - mRoute.removeInstruction(instruction); + Route.Instruction instruction = viewModel.route.getNearestInstruction(mMapPosition.getGeoPoint()); + viewModel.route.removeInstruction(instruction); MarkerItem marker = mPointLayer.getByUid(instruction); mPointLayer.removeItem(marker); - mPointHistory.remove(instruction); + viewModel.pointHistory.remove(instruction); mMapHolder.getMap().updateMap(true); mMapPosition = new MapPosition(); updateTrackMeasurements(); } }); - ImageButton undoButton = rootView.findViewById(R.id.undoButton); - undoButton.setOnClickListener(v -> { - if (!mPointHistory.isEmpty()) { - GeoPoint point = mPointHistory.pop(); - Route.Instruction instruction = mRoute.getNearestInstruction(point); + viewBinding.undoButton.setOnClickListener(v -> { + if (!viewModel.pointHistory.isEmpty()) { + GeoPoint point = viewModel.pointHistory.pop(); + Route.Instruction instruction = viewModel.route.getNearestInstruction(point); if (instruction == null) { - mPointHistory.clear(); + viewModel.pointHistory.clear(); return; } - mRoute.removeInstruction(instruction); + viewModel.route.removeInstruction(instruction); MarkerItem marker = mPointLayer.getByUid(instruction); mPointLayer.removeItem(marker); mMapHolder.getMap().updateMap(true); @@ -134,8 +127,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa updateTrackMeasurements(); } }); - - return rootView; } @Override @@ -151,12 +142,12 @@ public void onAttach(@NonNull Context context) { @Override public void onStart() { super.onStart(); - mRouteLayer = new RouteLayer(mMapHolder.getMap(), Color.RED, 5, mRoute); + mRouteLayer = new RouteLayer(mMapHolder.getMap(), Color.RED, 5, viewModel.route); mMapHolder.getMap().layers().add(mRouteLayer); Bitmap bitmap = new AndroidBitmap(MarkerFactory.getMarkerSymbol(requireContext(), R.drawable.dot_black, Color.RED)); MarkerSymbol symbol = new MarkerSymbol(bitmap, MarkerItem.HotspotPlace.CENTER); - ArrayList items = new ArrayList<>(mRoute.length()); - for (GeoPoint point : mRoute.getCoordinates()) { + ArrayList items = new ArrayList<>(viewModel.route.length()); + for (GeoPoint point : viewModel.route.getCoordinates()) { items.add(new MarkerItem(point, null, null, point)); } mPointLayer = new ItemizedLayer<>(mMapHolder.getMap(), items, symbol, MapTrek.density, this); @@ -198,14 +189,12 @@ public void onDetach() { public void onDestroy() { super.onDestroy(); mMapPosition = null; - mPointHistory = null; - mRoute = null; } private void updateTrackMeasurements() { - int length = Math.max(mRoute.length() - 1, 0); - mDistanceView.setText(StringFormatter.distanceHP(mRoute.distance)); - mSizeView.setText(getResources().getQuantityString(R.plurals.numberOfSegments, length, length)); + int length = Math.max(viewModel.route.length() - 1, 0); + viewBinding.distance.setText(StringFormatter.distanceHP(viewModel.route.distance)); + viewBinding.size.setText(getResources().getQuantityString(R.plurals.numberOfSegments, length, length)); } @Override @@ -217,4 +206,9 @@ public boolean onItemSingleTapUp(int index, MarkerItem item) { public boolean onItemLongPress(int index, MarkerItem item) { return false; } + + public static class RulerViewModel extends ViewModel { + private final Route route = new Route(); + private final Stack pointHistory = new Stack<>(); + } } diff --git a/app/src/main/java/mobi/maptrek/layers/MapCoverageLayer.java b/app/src/main/java/mobi/maptrek/layers/MapCoverageLayer.java index 649e35ee..cceb37ee 100644 --- a/app/src/main/java/mobi/maptrek/layers/MapCoverageLayer.java +++ b/app/src/main/java/mobi/maptrek/layers/MapCoverageLayer.java @@ -93,7 +93,7 @@ public class MapCoverageLayer extends AbstractVectorLayer implements Ge private final Bitmap mHillshadesBitmap; private final Bitmap mPresentHillshadesBitmap; private final java.text.DateFormat mDateFormat; - private Context mContext; + private final Context mContext; public MapCoverageLayer(Context context, Map map, Index mapIndex, float scale) { super(map); @@ -342,6 +342,10 @@ public void onHasDownloadSizes() { update(); } + @Override + public void onBaseMapChanged() { + } + @Override public void onStatsChanged() { update(); diff --git a/app/src/main/java/mobi/maptrek/maps/maptrek/Index.java b/app/src/main/java/mobi/maptrek/maps/maptrek/Index.java index 2ef31592..d46e7412 100644 --- a/app/src/main/java/mobi/maptrek/maps/maptrek/Index.java +++ b/app/src/main/java/mobi/maptrek/maps/maptrek/Index.java @@ -256,7 +256,7 @@ public void selectNativeMap(int x, int y, ACTION action) { for (WeakReference weakRef : mMapStateListeners) { MapStateListener mapStateListener = weakRef.get(); if (mapStateListener != null) { - mapStateListener.onMapSelected(x, y, mapStatus.action, stats); + mapStateListener.onMapSelected(x, y, action == ACTION.CANCEL ? action : mapStatus.action, stats); } } } @@ -394,6 +394,12 @@ public void clearSelections() { for (int y = 0; y < 128; y++) if (mMaps[x][y] != null) mMaps[x][y].action = ACTION.NONE; + for (WeakReference weakRef : mMapStateListeners) { + MapStateListener mapStateListener = weakRef.get(); + if (mapStateListener != null) { + mapStateListener.onStatsChanged(); + } + } } public void cancelDownload(int x, int y) { @@ -856,6 +862,12 @@ private void setDownloaded(int x, int y, short date) { } if (x == -1 && y == -1) { mBaseMapVersion = date; + for (WeakReference weakRef : mMapStateListeners) { + MapStateListener mapStateListener = weakRef.get(); + if (mapStateListener != null) { + mapStateListener.onBaseMapChanged(); + } + } } else if (x >= 0 && y >= 0) { MapStatus mapStatus = getNativeMap(x, y); mapStatus.created = date; @@ -1015,6 +1027,7 @@ public static class IndexStats { public interface MapStateListener { void onHasDownloadSizes(); + void onBaseMapChanged(); void onStatsChanged(); void onHillshadeAccountingChanged(boolean account); diff --git a/app/src/main/java/mobi/maptrek/viewmodels/AmenityViewModel.java b/app/src/main/java/mobi/maptrek/viewmodels/AmenityViewModel.java new file mode 100644 index 00000000..9dc96c40 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/viewmodels/AmenityViewModel.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Andrey Novikov + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + * + */ + +package mobi.maptrek.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import mobi.maptrek.data.Amenity; + +public class AmenityViewModel extends ViewModel { + private final MutableLiveData amenity = new MutableLiveData<>(null); + public LiveData getAmenity() { + return amenity; + } + public void setAmenity(Amenity amenity) { + this.amenity.setValue(amenity); + } +} diff --git a/app/src/main/java/mobi/maptrek/viewmodels/MapIndexViewModel.java b/app/src/main/java/mobi/maptrek/viewmodels/MapIndexViewModel.java new file mode 100644 index 00000000..318132f9 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/viewmodels/MapIndexViewModel.java @@ -0,0 +1,310 @@ +/* + * Copyright 2024 Andrey Novikov + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + * + */ + +package mobi.maptrek.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import mobi.maptrek.Configuration; +import mobi.maptrek.MapTrek; +import mobi.maptrek.maps.maptrek.Index; + +public class MapIndexViewModel extends ViewModel implements Index.MapStateListener { + private static final Logger logger = LoggerFactory.getLogger(MapIndexViewModel.class); + + private static final long INDEX_CACHE_TIMEOUT = 24 * 3600 * 1000L; // One day + private static final long INDEX_CACHE_EXPIRATION = 60 * 24 * 3600 * 1000L; // Two months + private static final long HILLSHADE_CACHE_TIMEOUT = 60 * 24 * 3600 * 1000L; // Two months + + public final Index nativeIndex = MapTrek.getApplication().getMapIndex(); + public final File cacheFile; + private final File hillshadeCacheFile; + + private final MutableLiveData indexState = new MutableLiveData<>(nativeIndex.getMapStats()); + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + public MapIndexViewModel() { + super(); + boolean hillshadesEnabled = Configuration.getHillshadesEnabled(); + nativeIndex.accountHillshades(hillshadesEnabled); + nativeIndex.addMapStateListener(this); + File cacheDir = MapTrek.getApplication().getExternalCacheDir(); + cacheFile = new File(cacheDir, "mapIndex"); + hillshadeCacheFile = new File(cacheDir, "hillshadeIndex"); + } + + @Override + protected void onCleared() { + super.onCleared(); + nativeIndex.removeMapStateListener(this); + executorService.shutdown(); + } + + public LiveData getNativeIndexState() { + return indexState; + } + + public static class BaseMapState { + public int version; + public long size; + public boolean outdated; + + public BaseMapState(int version, long size, boolean outdated) { + this.version = version; + this.size = size; + this.outdated = outdated; + } + } + + private final MutableLiveData baseMapState = new MutableLiveData<>( + new BaseMapState( + nativeIndex.getBaseMapVersion(), + nativeIndex.getBaseMapSize(), + nativeIndex.isBaseMapOutdated() + ) + ); + public LiveData getBaseMapState() { + return baseMapState; + } + + public static class ActionState { + public int x; + public int y; + public Index.ACTION action; + + public ActionState(int x, int y, Index.ACTION action) { + this.x = x; + this.y = y; + this.action = action; + } + } + + private final MutableLiveData actionState = new MutableLiveData<>(null); + public LiveData getActionState() { + return actionState; + } + + @Override + public void onHasDownloadSizes() { + indexState.postValue(nativeIndex.getMapStats()); + } + + @Override + public void onBaseMapChanged() { + baseMapState.postValue( + new BaseMapState( + nativeIndex.getBaseMapVersion(), + nativeIndex.getBaseMapSize(), + nativeIndex.isBaseMapOutdated() + ) + ); + } + + @Override + public void onStatsChanged() { + indexState.postValue(nativeIndex.getMapStats()); + } + + @Override + public void onHillshadeAccountingChanged(boolean account) { + indexState.postValue(nativeIndex.getMapStats()); + } + + @Override + public void onMapSelected(int x, int y, Index.ACTION action, Index.IndexStats stats) { + actionState.postValue(new ActionState(x, y, action)); + indexState.postValue(stats); + } + + private int progress = -1; + private final MutableLiveData indexDownloadProgressState = new MutableLiveData<>(progress); + public LiveData getIndexDownloadProgressState() { + return indexDownloadProgressState; + } + + public void loadMapIndexes(String stats) { + if (nativeIndex.hasDownloadSizes()) + return; + if (progress >= 0) // already loading + return; + + progress = 0; + indexDownloadProgressState.setValue(0); + + executorService.execute(() -> { + boolean result = doLoadMapIndexes(stats); + if (result) { + boolean expired = cacheFile.lastModified() + INDEX_CACHE_EXPIRATION < System.currentTimeMillis(); + progress = -1; + indexDownloadProgressState.postValue(progress); + nativeIndex.setHasDownloadSizes(expired); + } else { + progress = -2; // error + indexDownloadProgressState.postValue(progress); + } + }); + } + + private boolean doLoadMapIndexes(String stats) { + long now = System.currentTimeMillis(); + boolean validCache = cacheFile.lastModified() + INDEX_CACHE_TIMEOUT > now; + boolean validHillshadeCache = hillshadeCacheFile.lastModified() + HILLSHADE_CACHE_TIMEOUT > now; + int divider = validHillshadeCache ? 1 : 2; + // load map index + try { + boolean loaded = false; + InputStream in; + if (!validCache) { + URL url = new URL(Index.getIndexUri().toString() + "?" + stats); + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) url.openConnection(); + in = urlConnection.getInputStream(); + File tmpFile = new File(cacheFile.getAbsoluteFile() + "_tmp"); + OutputStream out = new FileOutputStream(tmpFile); + loadMapIndex(in, out, divider); + loaded = tmpFile.renameTo(cacheFile); + } catch (IOException e) { + logger.error("Failed to download map index", e); + } finally { + if (urlConnection != null) + urlConnection.disconnect(); + } + } + if (!loaded) { + in = new FileInputStream(cacheFile); + loadMapIndex(in, null, divider); + } + } catch (Exception e) { + logger.error("Failed to load map index", e); + // remove cache on any error + //noinspection ResultOfMethodCallIgnored + cacheFile.delete(); + return false; + } + // load hillshade index + try { + boolean loaded = false; + InputStream in; + if (!validHillshadeCache) { + URL url = new URL(Index.getHillshadeIndexUri().toString()); + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) url.openConnection(); + in = urlConnection.getInputStream(); + File tmpFile = new File(hillshadeCacheFile.getAbsoluteFile() + "_tmp"); + OutputStream out = new FileOutputStream(hillshadeCacheFile); + loadHillshadesIndex(in, out, divider); + loaded = tmpFile.renameTo(hillshadeCacheFile); + } catch (IOException e) { + logger.error("Failed to download hillshades index", e); + } finally { + if (urlConnection != null) + urlConnection.disconnect(); + } + } + if (!loaded) { + in = new FileInputStream(hillshadeCacheFile); + loadHillshadesIndex(in, null, divider); + } + } catch (Exception e) { + logger.error("Failed to load hillshades index", e); + // remove cache on any error + //noinspection ResultOfMethodCallIgnored + hillshadeCacheFile.delete(); + return false; + } + return true; + } + + private void loadMapIndex(InputStream in, OutputStream out, int divider) throws IOException { + DataInputStream data = new DataInputStream(new BufferedInputStream(in)); + DataOutputStream dataOut = null; + if (out != null) + dataOut = new DataOutputStream(new BufferedOutputStream(out)); + + for (int x = 0; x < 128; x++) + for (int y = 0; y < 128; y++) { + short date = data.readShort(); + int size = data.readInt(); + if (dataOut != null) { + dataOut.writeShort(date); + dataOut.writeInt(size); + } + nativeIndex.setNativeMapStatus(x, y, date, size); + int p = (int) ((x * 128 + y) / 163.84 / divider); + if (p > progress) { + progress = p; + indexDownloadProgressState.postValue(progress); + } + } + short date = data.readShort(); + int size = data.readInt(); + nativeIndex.setBaseMapStatus(date, size); + if (dataOut != null) { + dataOut.writeShort(date); + dataOut.writeInt(size); + dataOut.close(); + } + } + + private void loadHillshadesIndex(InputStream in, OutputStream out, int divider) throws IOException { + DataInputStream data = new DataInputStream(new BufferedInputStream(in)); + DataOutputStream dataOut = null; + if (out != null) + dataOut = new DataOutputStream(new BufferedOutputStream(out)); + + for (int x = 0; x < 128; x++) + for (int y = 0; y < 128; y++) { + byte version = data.readByte(); + int size = data.readInt(); + if (dataOut != null) { + dataOut.writeByte(version); + dataOut.writeInt(size); + } + nativeIndex.setHillshadeStatus(x, y, version, size); + int p = (int) ((x * 128 + y) / 163.84 / divider); + if (p > progress) { + progress = p; + indexDownloadProgressState.postValue(progress); + } + } + if (dataOut != null) { + dataOut.close(); + } + } +} diff --git a/app/src/main/java/mobi/maptrek/viewmodels/MapViewModel.java b/app/src/main/java/mobi/maptrek/viewmodels/MapViewModel.java new file mode 100644 index 00000000..b169f6a9 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/viewmodels/MapViewModel.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Andrey Novikov + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + * + */ + +package mobi.maptrek.viewmodels; + +import android.location.Location; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.oscim.core.GeoPoint; + +public class MapViewModel extends ViewModel { + public static class MarkerState { + private final GeoPoint coordinates; + private final String name; + private final boolean amenity; + private final boolean shown; + + public MarkerState(GeoPoint coordinates, String name, boolean amenity, boolean shown) { + this.coordinates = coordinates; + this.name = name; + this.amenity = amenity; + this.shown = shown; + } + + public GeoPoint getCoordinates() { + return coordinates; + } + + public String getName() { + return name; + } + + public boolean isAmenity() { + return amenity; + } + + public boolean isShown() { + return shown; + } + } + + private final MutableLiveData location = new MutableLiveData<>(new Location("unknown")); + public LiveData getLocation() { + return location; + } + public void setLocation(Location location) { + this.location.setValue(location); + } + + public void showMarker(@NonNull GeoPoint coordinates, String name, boolean amenity) { + markerState.setValue(new MarkerState(coordinates, name, amenity, true)); + } + + public void removeMarker() { + markerState.setValue(new MarkerState(null, null, false, false)); + } + + private final MutableLiveData markerState = new MutableLiveData<>(new MarkerState(null, null, false, false)); + public LiveData getMarkerState() { + return markerState; + } +} diff --git a/app/src/main/res/layout/fragment_map_selection.xml b/app/src/main/res/layout/fragment_map_selection.xml index 8cf8eded..d820fdc6 100644 --- a/app/src/main/res/layout/fragment_map_selection.xml +++ b/app/src/main/res/layout/fragment_map_selection.xml @@ -43,6 +43,13 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:visibility="gone" /> + +