Workout: offline map init

This commit is contained in:
José Rebelo
2025-04-04 22:02:46 +00:00
committed by José Rebelo
parent 732cbd9b16
commit 8cec2282a8
17 changed files with 576 additions and 75 deletions
+7
View File
@@ -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'
+8
View File
@@ -173,6 +173,14 @@
android:name=".activities.DashboardPreferencesActivity"
android:label="@string/dashboard_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.maps.MapsTrackActivity"
android:label="@string/maps"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.maps.MapsSettingsActivity"
android:label="@string/maps_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.SleepAsAndroidPreferencesActivity"
android:label="@string/sleepasandroid_settings"
@@ -16,9 +16,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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<GPSCoordinate> points = getActivityPoints(inputFile)
.stream()
.map(ActivityPoint::getLocation)
.filter(Objects::nonNull)
.collect(Collectors.toList());
final ArrayList<GPSCoordinate> 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<ActivityPoint> getActivityPoints(final File trackFile) {
final List<ActivityPoint> 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<? extends GPSCoordinate> trackPoints) {
private void drawTrack(MapView mapView, final ArrayList<? extends GPSCoordinate> 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<LatLong> 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;
}
}
@@ -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 -> {
@@ -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 <https://www.gnu.org/licenses/>. */
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();
}
}
@@ -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 <https://www.gnu.org/licenses/>. */
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<Uri> 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;
});
}
}
@@ -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 <https://www.gnu.org/licenses/>. */
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<? extends GPSCoordinate> 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<LatLong> 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);
}
}
@@ -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<GPSCoordinate> CREATOR = new Creator<GPSCoordinate>() {
@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<GPSCoordinate> {
@Override
public int compare(GPSCoordinate trkPt1, GPSCoordinate trkPt2) {
@@ -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) {
}
}
@@ -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 <https://www.gnu.org/licenses/>. */
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();
}
}
+12
View File
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#7E7E7E"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z" />
</vector>
@@ -0,0 +1,13 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity">
<org.mapsforge.map.android.view.MapView
android:id="@+id/activitygpsview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</RelativeLayout>
+24 -2
View File
@@ -18,11 +18,33 @@
android:text="@string/gps_track"
android:textSize="16sp" />
<ImageView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/gpsWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Maps not found" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:orientation="vertical">
<org.mapsforge.map.android.view.MapView
android:id="@+id/activitygpsview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:src="@tools:sample/avatars" />
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
+4 -3
View File
@@ -3786,7 +3786,6 @@
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
<string name="huawei_stress_test_enable">Automatic stress test</string>
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>
@@ -3801,10 +3800,12 @@
<string name="huawei_stress_calibrate_finish">Finish</string>
<string name="huawei_stress_calibrate_again">Calibrate again</string>
<string name="huawei_stress_calibrate_adjust_score">Adjust score</string>
<string name="banglejs_notification_missed_call_title">Missed call</string>
<string name="banglejs_notification_missed_call_source">Incoming call</string>
<string name="banglejs_notification_missed_call_suppressed_caller_id">Unknown caller. ID is suppressed</string>
<string name="huawei_voice_boost">Voice Boost</string>
<string name="maps">Maps</string>
<string name="maps_settings">Maps settings</string>
<string name="maps_download_title">Download Maps</string>
<string name="maps_download_summary">Click here to download map files. They should be saved to the folder selected in the preference below.</string>
</resources>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_download"
android:key="maps_download"
android:persistent="false"
android:summary="@string/maps_download_summary"
android:title="@string/maps_download_title" />
<Preference
android:icon="@drawable/ic_folder"
android:key="maps_folder"
android:title="@string/folder" />
</PreferenceScreen>
+5
View File
@@ -326,6 +326,11 @@
android:summary="@string/pref_summary_opentracks_packagename"
android:title="@string/pref_title_opentracks_packagename" />
<Preference
android:key="pref_category_maps"
android:title="@string/maps"
app:iconSpaceReserved="false" />
<Preference
android:icon="@drawable/ic_activity_sleep"
android:key="pref_category_sleepasandroid"
+6
View File
@@ -28,6 +28,12 @@
"com.github.wax911.android-emojify:*"
]
},
{
"groupName": "mapsforge",
"matchPackagePrefixes": [
"com.github.mapsforge.mapsforge"
]
},
{
"description": "solarpositioning v2 needs java 17, we need to stay on v0",
"allowedVersions": "<2.0",