diff --git a/app/build.gradle b/app/build.gradle index e8db9f9a71..3c20bd434b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,6 +248,13 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' implementation 'org.msgpack:msgpack-core:0.9.9' + implementation 'com.github.mapsforge.mapsforge:mapsforge-core:0.21.0' + implementation 'com.github.mapsforge.mapsforge:mapsforge-map:0.21.0' + implementation 'com.github.mapsforge.mapsforge:mapsforge-map-reader:0.21.0' + implementation 'com.github.mapsforge.mapsforge:mapsforge-themes:0.21.0' + implementation 'com.github.mapsforge.mapsforge:mapsforge-map-android:0.21.0' + implementation 'com.caverock:androidsvg:1.4' + // Bouncy Castle is included directly in GB, to avoid pulling the entire dependency // It's included in the org.bouncycastle.shaded package, to fix conflicts with roboelectric //implementation 'org.bouncycastle:bcpkix-jdk18on:1.76' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7523d51543..9c281d6bb3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -173,6 +173,14 @@ android:name=".activities.DashboardPreferencesActivity" android:label="@string/dashboard_settings" android:parentActivityName=".activities.SettingsActivity" /> + + . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Paint; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -27,6 +27,11 @@ import android.widget.ImageView; import androidx.annotation.Nullable; +import org.mapsforge.core.graphics.Paint; +import org.mapsforge.core.model.LatLong; +import org.mapsforge.map.android.graphics.AndroidGraphicFactory; +import org.mapsforge.map.android.view.MapView; +import org.mapsforge.map.layer.overlay.Polyline; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,9 +43,11 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity; import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile; @@ -48,21 +55,28 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDat import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException; import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser; +import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager; import static android.graphics.Bitmap.createBitmap; public class ActivitySummariesGpsFragment extends AbstractGBFragment { private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class); - private ImageView gpsView; private final int CANVAS_SIZE = 360; + private MapView mapView; private File inputFile; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_gps, container, false); - gpsView = rootView.findViewById(R.id.activitygpsview); + mapView = rootView.findViewById(R.id.activitygpsview); + //mapView.getMapScaleBar().setVisible(true); + mapView.setBuiltInZoomControls(false); + + MapsManager mapsManager = new MapsManager(requireContext()); + mapsManager.loadMaps(mapView); + if (inputFile != null) { processInBackgroundThread(); } @@ -71,88 +85,83 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment { public void set_data(File inputFile) { this.inputFile = inputFile; - if (gpsView != null) { //first fragment inflate is AFTER this is called + if (mapView != null) { //first fragment inflate is AFTER this is called processInBackgroundThread(); } } private void processInBackgroundThread() { - final Canvas canvas = createCanvas(gpsView); new Thread(() -> { - final List points = getActivityPoints(inputFile) - .stream() - .map(ActivityPoint::getLocation) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + final ArrayList points = new ArrayList<>(); + if (inputFile.getName().endsWith(".gpx")) { + try (FileInputStream inputStream = new FileInputStream(inputFile)) { + final GpxParser gpxParser = new GpxParser(inputStream); + points.addAll(gpxParser.getGpxFile().getPoints()); + } catch (final IOException e) { + LOG.error("Failed to open {}", inputFile, e); + return; + } catch (final GpxParseException e) { + LOG.error("Failed to parse gpx file", e); + return; + } + } else if (inputFile.getName().endsWith(".fit")) { + try { + FitFile fitFile = FitFile.parseIncoming(inputFile); + for (final RecordData record : fitFile.getRecords()) { + if (record instanceof FitRecord) { + final ActivityPoint activityPoint = ((FitRecord) record).toActivityPoint(); + if (activityPoint.getLocation() != null) { + points.add(activityPoint.getLocation()); + } + } + } + } catch (final IOException e) { + LOG.error("Failed to open {}", inputFile, e); + return; + } catch (final Exception e) { + LOG.error("Failed to parse fit file", e); + return; + } + } else { + LOG.warn("Unknown file type {}", inputFile.getName()); + return; + } if (!points.isEmpty()) { - drawTrack(canvas, points); + drawTrack(mapView, points); } }).start(); } - public static List getActivityPoints(final File trackFile) { - final List points = new ArrayList<>(); - if (trackFile == null) { - return points; - } - if (trackFile.getName().endsWith(".gpx")) { - try (FileInputStream inputStream = new FileInputStream(trackFile)) { - final GpxParser gpxParser = new GpxParser(inputStream); - points.addAll(gpxParser.getGpxFile().getActivityPoints()); - } catch (final IOException e) { - LOG.error("Failed to open {}", trackFile, e); - } catch (final GpxParseException e) { - LOG.error("Failed to parse gpx file", e); - } - } else if (trackFile.getName().endsWith(".fit")) { - try { - FitFile fitFile = FitFile.parseIncoming(trackFile); - for (final RecordData record : fitFile.getRecords()) { - if (record instanceof FitRecord) { - points.add(((FitRecord) record).toActivityPoint()); - } - } - } catch (final IOException e) { - LOG.error("Failed to open {}", trackFile, e); - } catch (final Exception e) { - LOG.error("Failed to parse fit file", e); - } - } else { - LOG.warn("Unknown file type {}", trackFile.getName()); - } - - return points; - } - - private void drawTrack(Canvas canvas, List trackPoints) { + private void drawTrack(MapView mapView, final ArrayList trackPoints) { double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude(); double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude(); - double maxAlt = (Collections.max(trackPoints, new GPSCoordinate.compareElevation())).getAltitude(); - double minAlt = (Collections.min(trackPoints, new GPSCoordinate.compareElevation())).getAltitude(); - float scale_factor_w = (float) ((maxLon - minLon) / (maxLat - minLat)); - float scale_factor_h = (float) ((maxLat - minLat) / (maxLon - minLon)); + List latlongs = trackPoints.stream() + .map(p -> new LatLong(p.getLatitude(), p.getLongitude())) + .collect(Collectors.toList()); - if (scale_factor_h > scale_factor_w) { //scaling to draw proportionally - scale_factor_h = 1; - } else { - scale_factor_w = 1; - } + Paint p = AndroidGraphicFactory.INSTANCE.createPaint(); + p.setColor(R.color.hrv_status_char_line_color); + Polyline polyline = new Polyline(p, AndroidGraphicFactory.INSTANCE); + polyline.setPoints(latlongs); + mapView.addLayer(polyline); + mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2)); + mapView.setZoomLevel((byte) 12); - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setStrokeWidth(1); - paint.setColor(getResources().getColor(R.color.chart_activity_light)); - - for (GPSCoordinate p : trackPoints) { - float lat = (float) ((p.getLatitude() - minLat) / (maxLat - minLat)); - float lon = (float) ((p.getLongitude() - minLon) / (maxLon - minLon)); - float alt = (float) ((p.getAltitude() - minAlt) / (maxAlt - minAlt)); - paint.setStrokeWidth(1 + alt); //make thicker with higher altitude, we could do more here - canvas.drawPoint(CANVAS_SIZE * lon * scale_factor_w, CANVAS_SIZE * lat * scale_factor_h, paint); - } + mapView.setOnTouchListener((a, b) -> { + final Intent startIntent = new Intent(requireContext(), MapsTrackActivity.class); + startIntent.putParcelableArrayListExtra("points", trackPoints); + requireContext().startActivity(startIntent); + return true; + }); + mapView.setOnClickListener(v -> { + final Intent startIntent = new Intent(requireContext(), MapsTrackActivity.class); + startIntent.putParcelableArrayListExtra("points", trackPoints); + requireContext().startActivity(startIntent); + }); } private Canvas createCanvas(ImageView imageView) { @@ -178,6 +187,4 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment { protected CharSequence getTitle() { return null; } - } - diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 4483dc1725..2e0d22fb71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsActivity; import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver; import nodomain.freeyourgadget.gadgetbridge.model.Weather; @@ -372,6 +373,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 { }); } + pref = findPreference("pref_category_maps"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + Intent enableIntent = new Intent(requireContext(), MapsSettingsActivity.class); + startActivity(enableIntent); + return true; + }); + } + pref = findPreference("pref_category_sleepasandroid"); if (pref != null) { pref.setOnPreferenceClickListener(preference -> { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsActivity.java new file mode 100644 index 0000000000..5c2ec45a6d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsActivity.java @@ -0,0 +1,33 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.maps; + +import androidx.preference.PreferenceFragmentCompat; + +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2; + +public class MapsSettingsActivity extends AbstractSettingsActivityV2 { + @Override + protected String fragmentTag() { + return MapsSettingsFragment.FRAGMENT_TAG; + } + + @Override + protected PreferenceFragmentCompat newFragment() { + return new MapsSettingsFragment(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsFragment.java new file mode 100644 index 0000000000..f68367ee37 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsSettingsFragment.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.maps; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import java.util.Objects; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment; +import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager; + +public class MapsSettingsFragment extends AbstractPreferenceFragment { + static final String FRAGMENT_TAG = "MAP_SETTINGS_FRAGMENT"; + + @Override + public void onCreatePreferences(@Nullable final Bundle savedInstanceState, @Nullable final String rootKey) { + setPreferencesFromResource(R.xml.map_settings, rootKey); + + final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + if (prefs == null) { + requireActivity().finish(); + return; + } + + final Preference prefDownload = Objects.requireNonNull(findPreference("maps_download")); + prefDownload.setOnPreferenceClickListener(preference -> { + startActivity(new Intent( + Intent.ACTION_VIEW, + Uri.parse("https://ftp-stud.hs-esslingen.de/pub/Mirrors/download.mapsforge.org/maps/v5/") + )); + return true; + }); + + final Preference prefFolder = Objects.requireNonNull(findPreference(MapsManager.PREF_MAPS_FOLDER)); + final ActivityResultLauncher mapsFolderChooser = registerForActivityResult( + new ActivityResultContracts.OpenDocumentTree(), + localUri -> { + LOG.info("Maps folder: {}", localUri); + if (localUri != null) { + requireContext().getContentResolver().takePersistableUriPermission(localUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + prefs.edit() + .putString(MapsManager.PREF_MAPS_FOLDER, localUri.toString()) + .apply(); + prefFolder.setSummary(localUri.toString()); + } + } + ); + final String currentFolder = prefs.getString(MapsManager.PREF_MAPS_FOLDER, ""); + prefFolder.setSummary(currentFolder); + prefFolder.setOnPreferenceClickListener(preference -> { + mapsFolderChooser.launch(null); + return true; + }); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsTrackActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsTrackActivity.java new file mode 100644 index 0000000000..09536114b5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/maps/MapsTrackActivity.java @@ -0,0 +1,92 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.maps; + +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.NonNull; + +import org.mapsforge.core.graphics.Paint; +import org.mapsforge.core.model.LatLong; +import org.mapsforge.map.android.graphics.AndroidGraphicFactory; +import org.mapsforge.map.android.view.MapView; +import org.mapsforge.map.layer.overlay.Polyline; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; +import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager; + +public class MapsTrackActivity extends AbstractGBActivity { + private MapView mapView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_maps_track); + + mapView = findViewById(R.id.activitygpsview); + MapsManager mapsManager = new MapsManager(this); + mapsManager.loadMaps(mapView); + + final ArrayList trackPoints = getIntent().getParcelableArrayListExtra("points"); + + double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); + double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); + double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude(); + double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude(); + List latlongs = trackPoints.stream() + .map(p -> new LatLong(p.getLatitude(), p.getLongitude())) + .collect(Collectors.toList()); + + Paint p = AndroidGraphicFactory.INSTANCE.createPaint(); + p.setColor(R.color.hrv_status_char_line_color); + Polyline polyline = new Polyline(p, AndroidGraphicFactory.INSTANCE); + polyline.setPoints(latlongs); + mapView.addLayer(polyline); + + mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2)); + mapView.setZoomLevel((byte) 12); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + // back button + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onSaveInstanceState(@NonNull final Bundle state) { + super.onSaveInstanceState(state); + } + + @Override + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java index 14744ab0b2..63a966a822 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java @@ -17,6 +17,10 @@ package nodomain.freeyourgadget.gadgetbridge.model; import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; import androidx.annotation.NonNull; @@ -24,7 +28,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Comparator; -public class GPSCoordinate { +public class GPSCoordinate implements Parcelable { private final double latitude; private final double longitude; private final double altitude; @@ -50,6 +54,12 @@ public class GPSCoordinate { this(longitude, latitude, UNKNOWN_ALTITUDE); } + protected GPSCoordinate(Parcel in) { + latitude = in.readDouble(); + longitude = in.readDouble(); + altitude = in.readDouble(); + } + public double getLatitude() { return latitude; } @@ -156,6 +166,30 @@ public class GPSCoordinate { return "lon: " + formatLocation(longitude) + ", lat: " + formatLocation(latitude) + ", alt: " + formatLocation(altitude) + "m"; } + @Override + public void writeToParcel(@NonNull final Parcel dest, final int flags) { + dest.writeDouble(latitude); + dest.writeDouble(longitude); + dest.writeDouble(altitude); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public GPSCoordinate createFromParcel(Parcel in) { + return new GPSCoordinate(in); + } + + @Override + public GPSCoordinate[] newArray(int size) { + return new GPSCoordinate[size]; + } + }; + public static class compareLatitude implements Comparator { @Override public int compare(GPSCoordinate trkPt1, GPSCoordinate trkPt2) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapTheme.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapTheme.java new file mode 100644 index 0000000000..7e71f37954 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapTheme.java @@ -0,0 +1,50 @@ +package nodomain.freeyourgadget.gadgetbridge.util.maps; + +import org.mapsforge.map.rendertheme.XmlRenderTheme; +import org.mapsforge.map.rendertheme.XmlRenderThemeMenuCallback; +import org.mapsforge.map.rendertheme.XmlThemeResourceProvider; + +import java.io.InputStream; + +public enum MapTheme implements XmlRenderTheme { + DEFAULT("/assets/mapsforge/default.xml"), + OSMARENDER("/assets/mapsforge/osmarender.xml"), + ; + + private XmlRenderThemeMenuCallback menuCallback; + private final String path; + + MapTheme(String path) { + this.path = path; + } + + @Override + public XmlRenderThemeMenuCallback getMenuCallback() { + return menuCallback; + } + + @Override + public String getRelativePathPrefix() { + return "/assets/"; + } + + @Override + public InputStream getRenderThemeAsStream() { + return getClass().getResourceAsStream(this.path); + } + + @Override + public XmlThemeResourceProvider getResourceProvider() { + return null; + } + + @Override + public void setMenuCallback(final XmlRenderThemeMenuCallback menuCallback) { + this.menuCallback = menuCallback; + } + + @Override + public void setResourceProvider(final XmlThemeResourceProvider resourceProvider) { + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapsManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapsManager.java new file mode 100644 index 0000000000..158d721b74 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/maps/MapsManager.java @@ -0,0 +1,109 @@ +/* Copyright (C) 2024 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.util.maps; + +import android.content.Context; +import android.net.Uri; +import android.widget.Toast; + +import androidx.documentfile.provider.DocumentFile; + +import org.mapsforge.map.android.graphics.AndroidGraphicFactory; +import org.mapsforge.map.android.util.AndroidUtil; +import org.mapsforge.map.android.view.MapView; +import org.mapsforge.map.datastore.MultiMapDataStore; +import org.mapsforge.map.layer.cache.TileCache; +import org.mapsforge.map.layer.renderer.TileRendererLayer; +import org.mapsforge.map.reader.MapFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; + +public final class MapsManager { + private static final Logger LOG = LoggerFactory.getLogger(MapsManager.class); + + public static final String PREF_MAPS_FOLDER = "maps_folder"; + + private final Context mContext; + + public MapsManager(final Context context) { + this.mContext = context; + } + + public void loadMaps(final MapView mapView) { + AndroidGraphicFactory.createInstance(GBApplication.app()); + GBPrefs prefs = GBApplication.getPrefs(); + + String folderUri = prefs.getString(PREF_MAPS_FOLDER, ""); + if (folderUri.isEmpty()) { + return; + } + + final DocumentFile folder = DocumentFile.fromTreeUri(mContext, Uri.parse(folderUri)); + if (folder == null || folder.listFiles().length == 0) { + return; + } + + MultiMapDataStore multiMapDataStore = new MultiMapDataStore(MultiMapDataStore.DataPolicy.RETURN_ALL); + + final DocumentFile[] documentFiles = folder.listFiles(); + + LOG.debug("Got {} map files", documentFiles.length); + + for (final DocumentFile documentFile : documentFiles) { + if (!documentFile.canRead()) { + continue; + } + assert documentFile.getName() != null; + if (!documentFile.getName().endsWith(".map")) { + continue; + } + + LOG.debug("Loading {}", documentFile.getName()); + + try { + FileInputStream inputStream = (FileInputStream) mContext.getContentResolver().openInputStream(documentFile.getUri()); + assert inputStream != null; + MapFile mapFile = new MapFile(inputStream, 0, null); + multiMapDataStore.addMapDataStore(mapFile, true, true); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + TileCache tileCache = AndroidUtil.createTileCache(mContext, "mapcache", + mapView.getModel().displayModel.getTileSize(), 1f, + mapView.getModel().frameBufferModel.getOverdrawFactor()); + + TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, multiMapDataStore, + mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE); + tileRendererLayer.setXmlRenderTheme(MapTheme.DEFAULT); + + mapView.getLayerManager().getLayers().add(tileRendererLayer); + + // TODO on exit mapView.destroyAll(); + //AndroidGraphicFactory.clearResourceMemoryCache(); + } +} diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000000..e539b62dd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/activity_maps_track.xml b/app/src/main/res/layout/activity_maps_track.xml new file mode 100644 index 0000000000..dc63e48701 --- /dev/null +++ b/app/src/main/res/layout/activity_maps_track.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/fragment_gps.xml b/app/src/main/res/layout/fragment_gps.xml index 40109eede0..deb8a46d72 100644 --- a/app/src/main/res/layout/fragment_gps.xml +++ b/app/src/main/res/layout/fragment_gps.xml @@ -18,11 +18,33 @@ android:text="@string/gps_track" android:textSize="16sp" /> - + android:layout_marginBottom="20dp" + android:orientation="vertical"> + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af29aab1bd..c7aa1aedad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3786,7 +3786,6 @@ Activate airplane mode? airplane mode received unhandled response code 0x%x for operation 0x%x - Automatic stress test Enable or disable automatic stress test No initial data - calibrate first @@ -3801,10 +3800,12 @@ Finish Calibrate again Adjust score - Missed call Incoming call Unknown caller. ID is suppressed - Voice Boost + Maps + Maps settings + Download Maps + Click here to download map files. They should be saved to the folder selected in the preference below. diff --git a/app/src/main/res/xml/map_settings.xml b/app/src/main/res/xml/map_settings.xml new file mode 100644 index 0000000000..20211e7069 --- /dev/null +++ b/app/src/main/res/xml/map_settings.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b5729f5f54..59ad341bce 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -326,6 +326,11 @@ android:summary="@string/pref_summary_opentracks_packagename" android:title="@string/pref_title_opentracks_packagename" /> + +