diff --git a/build.gradle b/build.gradle index ef98a5623..bbf792596 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ ext { buildToolsVersion='29.0.3' // centrally manage some other dependencies - mapsforgeVersion = '0.13.0' + mapsforgeVersion = '0.14.0' osmdroidVersion = '6.1.6' // test dependencies diff --git a/cyclestreets.app/src/main/java/net/cyclestreets/CycleStreets.kt b/cyclestreets.app/src/main/java/net/cyclestreets/CycleStreets.kt index 6a4800426..60f10038b 100644 --- a/cyclestreets.app/src/main/java/net/cyclestreets/CycleStreets.kt +++ b/cyclestreets.app/src/main/java/net/cyclestreets/CycleStreets.kt @@ -5,8 +5,6 @@ import android.os.Bundle class CycleStreets : MainNavDrawerActivity(), RouteMapActivity, PhotoMapActivity { public override fun onCreate(savedInstanceState: Bundle?) { - MainSupport.switchMapFile(intent, this) - super.onCreate(savedInstanceState) MainSupport.handleLaunchIntent(intent, this) diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/ApiClient.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/ApiClient.kt index c3505af0b..0bf5b44b1 100644 --- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/ApiClient.kt +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/ApiClient.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageManager import net.cyclestreets.api.client.RetrofitApiClient import net.cyclestreets.core.R +import kotlin.collections.Map interface CycleStreetsApi { fun getJourneyJson(plan: String, leaving: String?, arriving: String?, speed: Int, lonLat: DoubleArray): String @@ -22,6 +23,7 @@ interface CycleStreetsApi { fun getPOIs(key: String, lonW: Double, latS: Double, lonE: Double, latN: Double): List fun getPOIs(key: String, lon: Double, lat: Double, radius: Int): List fun getBlogEntries(): Blog + fun getMaps(): Maps } object ApiClient : CycleStreetsApi { @@ -130,7 +132,9 @@ object ApiClient : CycleStreetsApi { override fun getBlogEntries(): Blog { return delegate.getBlogEntries() } - + override fun getMaps(): Maps { + return delegate.getMaps() + } } class ApiClientImpl(private val retrofitApiClient: RetrofitApiClient): CycleStreetsApi { @@ -230,6 +234,10 @@ class ApiClientImpl(private val retrofitApiClient: RetrofitApiClient): CycleStre return retrofitApiClient.blogEntries } + override fun getMaps(): Maps { + return retrofitApiClient.maps + } + ///////////////////////////////////////////////////// private fun itineraryPoints(vararg lonLat: Double): String { val sb = StringBuilder() diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/Maps.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/Maps.kt new file mode 100644 index 000000000..de0b120b5 --- /dev/null +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/Maps.kt @@ -0,0 +1,44 @@ +package net.cyclestreets.api + +import android.os.AsyncTask + +class Maps( + private val packs: Collection +): Iterable { + val size get() = packs.size + override operator fun iterator() = packs.iterator() + + companion object { + private var loaded_: Maps? = null + + fun get(): Maps? { + if (loaded_ == null) + backgroundLoad() + return loaded_ + } // get + + + private fun backgroundLoad() { + GetMapsTask().execute() + } + + private class GetMapsTask : AsyncTask() { + override fun doInBackground(vararg params: Void?): Maps? { + return load() + } + + override fun onPostExecute(maps: Maps?) { + loaded_ = maps + } + } + + private fun load(): Maps? { + try { + return ApiClient.getMaps() + } catch (e: Exception) { + println(e.message) + } + return null + } + } +} \ No newline at end of file diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/VectorMap.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/VectorMap.kt new file mode 100644 index 000000000..a217079c1 --- /dev/null +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/VectorMap.kt @@ -0,0 +1,10 @@ +package net.cyclestreets.api + +data class VectorMap( + val id: String, + val name: String, + val url: String, + val parent: String, + val size: String, + val lastModified: String +) \ No newline at end of file diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/RetrofitApiClient.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/RetrofitApiClient.java index 1691bb072..04a9a7474 100644 --- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/RetrofitApiClient.java +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/RetrofitApiClient.java @@ -7,6 +7,7 @@ import net.cyclestreets.api.Blog; import net.cyclestreets.api.GeoPlaces; +import net.cyclestreets.api.Maps; import net.cyclestreets.api.POI; import net.cyclestreets.api.POICategories; import net.cyclestreets.api.PhotomapCategories; @@ -24,6 +25,7 @@ import net.cyclestreets.api.client.dto.UserCreateResponseDto; import net.cyclestreets.api.client.dto.UserJourneysDto; import net.cyclestreets.api.client.geojson.GeoPlacesFactory; +import net.cyclestreets.api.client.geojson.MapsFactory; import net.cyclestreets.api.client.geojson.PhotosFactory; import net.cyclestreets.api.client.geojson.PoiFactory; @@ -222,6 +224,11 @@ public UserJourneys getUserJourneys(final String username) throws IOException { return response.body().toUserJourneys(); } + public Maps getMaps() throws IOException { + Response response = v2Api.getMaps().execute(); + return MapsFactory.Companion.toMaps(response.body()); + } + public Result register(final String username, final String password, final String name, diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/V2Api.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/V2Api.java index 5d14db85b..bd0bb94e6 100644 --- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/V2Api.java +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/V2Api.java @@ -49,6 +49,9 @@ Call geoCoder(@Query("q") String search, @GET("/v2/journeys.user?format=flat&datetime=friendly") Call getUserJourneys(@Query("username") String username); + @GET("/v2/mapdownloads.list") + Call getMaps(); + @FormUrlEncoded @POST("/v2/user.create") Call register(@Field("username") String username, diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/AbstractObjectFactory.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/AbstractObjectFactory.java index 51b7b1340..0bb397472 100644 --- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/AbstractObjectFactory.java +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/AbstractObjectFactory.java @@ -4,7 +4,7 @@ class AbstractObjectFactory { @SuppressWarnings("unchecked") - protected static V propertyOrDefault(Feature feature, String propertyName, V defaultValue) { + static V propertyOrDefault(Feature feature, String propertyName, V defaultValue) { return (feature.getProperty(propertyName) == null) ? defaultValue : (V)feature.getProperty(propertyName); } } diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/MapsFactory.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/MapsFactory.kt new file mode 100644 index 000000000..a466090c6 --- /dev/null +++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/api/client/geojson/MapsFactory.kt @@ -0,0 +1,37 @@ +package net.cyclestreets.api.client.geojson + +import net.cyclestreets.api.VectorMap +import net.cyclestreets.api.Maps +import net.cyclestreets.api.client.geojson.AbstractObjectFactory.propertyOrDefault +import org.geojson.Feature +import org.geojson.FeatureCollection + +class MapsFactory { + companion object { + fun toMaps(featureCollection: FeatureCollection): Maps { + return Maps( + featureCollection.features + .map { f -> toMap(f) } + .filter { m -> isBritainOrIreland(m) } + ) + } + + private fun toMap(feature: Feature): VectorMap { + return VectorMap( + feature.getProperty("id"), + feature.getProperty("name"), + feature.getProperty("url"), + propertyOrDefault(feature, "parent", ""), + propertyOrDefault(feature, "size", ""), + propertyOrDefault(feature, "lastModified", ""), + ) + } + + private fun isBritainOrIreland(m: VectorMap): Boolean { + return isBritainOrIreland(m.id) + } + private fun isBritainOrIreland(p: String): Boolean { + return p.contains("great-britain") || p.contains("ireland") + } + } +} diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt index 650f9c7b5..9eea30bf9 100644 --- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt +++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt @@ -16,13 +16,6 @@ private val TAG = Logging.getTag(MainSupport::class.java) object MainSupport { - fun switchMapFile(intent: Intent, context: Context): Boolean { - val mapPackage = intent.getStringExtra("mapfile") ?: return false - val pack = MapPack.findByPackage(context, mapPackage) ?: return false - CycleStreetsPreferences.enableMapFile(pack.path()) - return true - } - fun handleLaunchIntent(intent: Intent, activity: Activity): Boolean { val launchUri = intent.data ?: return false Log.d(TAG, "Handling launch intent with URI: $launchUri") diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt index e76b1aa88..1e1744c66 100644 --- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt +++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt @@ -20,24 +20,25 @@ import net.cyclestreets.util.MessageBox private val TAG = Logging.getTag(SettingsFragment::class.java) private const val PREFERENCE_SCREEN_ARG: String = "preferenceScreenArg" private val SETTINGS_ICONS = mapOf( - "screen-maps-display" to GoogleMaterial.Icon.gmd_map, - "mapstyle" to null, - "confirm-new-route" to null, - "screen-routing-preferences" to GoogleMaterial.Icon.gmd_directions, - "routetype" to null, - "speed" to null, - "units" to null, - "screen-liveride" to GoogleMaterial.Icon.gmd_navigation, - "nearing-turn-distance" to null, - "offtrack-distance" to null, - "replan-distance" to null, - "screen-locations" to GoogleMaterial.Icon.gmd_edit_location, - "screen-account" to GoogleMaterial.Icon.gmd_account_circle, - "cyclestreets-account" to null, - "username" to null, - "password" to null, - "uploadsize" to null, - "screen-about" to GoogleMaterial.Icon.gmd_info_outline + "screen-maps-display" to GoogleMaterial.Icon.gmd_map, + "mapstyle" to null, + "mapfile" to null, + "confirm-new-route" to null, + "screen-routing-preferences" to GoogleMaterial.Icon.gmd_directions, + "routetype" to null, + "speed" to null, + "units" to null, + "screen-liveride" to GoogleMaterial.Icon.gmd_navigation, + "nearing-turn-distance" to null, + "offtrack-distance" to null, + "replan-distance" to null, + "screen-locations" to GoogleMaterial.Icon.gmd_edit_location, + "screen-account" to GoogleMaterial.Icon.gmd_account_circle, + "cyclestreets-account" to null, + "username" to null, + "password" to null, + "uploadsize" to null, + "screen-about" to GoogleMaterial.Icon.gmd_info_outline ) @@ -49,11 +50,13 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP super.onCreate(savedInstance) setupMapStyles() + setupMapFileList() setSummary(CycleStreetsPreferences.PREF_ROUTE_TYPE_KEY) setSummary(CycleStreetsPreferences.PREF_UNITS_KEY) setSummary(CycleStreetsPreferences.PREF_SPEED_KEY) setSummary(CycleStreetsPreferences.PREF_MAPSTYLE_KEY) + setSummary(CycleStreetsPreferences.PREF_MAPFILE_KEY) setSummary(CycleStreetsPreferences.PREF_UPLOAD_SIZE) setSummary(CycleStreetsPreferences.PREF_NEARING_TURN) setSummary(CycleStreetsPreferences.PREF_OFFTRACK_DISTANCE) @@ -114,7 +117,28 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP private fun setupMapStyles() { findPreference(CycleStreetsPreferences.PREF_MAPSTYLE_KEY)?.apply { - TileSource.configurePreference(this) + + if (this.value == CycleStreetsPreferences.MAPSTYLE_MAPSFORGE && MapPack.availableMapPacks(context).isEmpty()) { + Log.i(TAG, "Offline Vector Maps were selected, but there are no available map packs; default to OSM") + this.value = CycleStreetsPreferences.MAPSTYLE_OSM + } + + TileSource.configurePreference(this) + } + } + + private fun setupMapFileList() { + findPreference(CycleStreetsPreferences.PREF_MAPFILE_KEY)?.apply { + populateMapFileList(this) + } + } + + private fun populateMapFileList(mapfilePref: ListPreference) { + context?.let { + val titles = MapPack.availableMapPacks(it).map { pack: MapPack -> pack.title } + val ids = MapPack.availableMapPacks(it).map { pack: MapPack -> pack.id } + mapfilePref.entries = titles.toTypedArray() + mapfilePref.entryValues = ids.toTypedArray() } } @@ -143,9 +167,36 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP prefUI.summary = prefUI.entry if (prefUI is EditTextPreference) prefUI.summary = prefUI.text - } - private fun setAccountSummary() { + if (CycleStreetsPreferences.PREF_MAPSTYLE_KEY == key) + setMapFileSummary((prefUI as ListPreference).value) + } + + private fun setMapFileSummary(style: String) { + val pref = findPreference(CycleStreetsPreferences.PREF_MAPFILE_KEY) ?: return + val mapfilePref = pref as ListPreference + + val enabled = style == CycleStreetsPreferences.MAPSTYLE_MAPSFORGE + mapfilePref.isEnabled = enabled + + if (!enabled) + return + + if (mapfilePref.entryValues.isEmpty()) { + mapfilePref.isEnabled = false + return + } + + val mapfile = CycleStreetsPreferences.mapfile() + var index = mapfilePref.findIndexOfValue(mapfile) + if (index == -1) + index = 0 // default to something + + mapfilePref.setValueIndex(index) + mapfilePref.summary = mapfilePref.entries[index] + } + + private fun setAccountSummary() { val pref = findPreference(CycleStreetsPreferences.PREF_ACCOUNT_KEY) ?: return val account = pref as PreferenceScreen diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/tiles/TileSource.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/tiles/TileSource.java index d0c81b40e..12efbd44e 100644 --- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/tiles/TileSource.java +++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/tiles/TileSource.java @@ -51,21 +51,19 @@ public static ITileSource mapRenderer(final Context context) { final ITileSource renderer = source.renderer(); if (renderer instanceof MapsforgeOSMTileSource) { - final String mapFile = CycleStreetsPreferences.mapfile(); - final MapPack pack = MapPack.findByPackage(context, mapFile); - if (pack.current()) - ((MapsforgeOSMTileSource)renderer).setMapFile(mapFile); - else { - MessageBox.YesNo(context, - R.string.tiles_map_pack_out_of_date, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface arg0, int arg1) { - MapPack.searchGooglePlay(context); - } - }); + final String mapId = CycleStreetsPreferences.mapfile(); + final MapPack pack = MapPack.findById(context, mapId); + if (pack == null) { CycleStreetsPreferences.resetMapstyle(); return source(DEFAULT_RENDERER).renderer(); } + + if (pack.getDownloaded()) + ((MapsforgeOSMTileSource)renderer).setMapFile(pack.getPath()); + else { + pack.download(context); + return source(DEFAULT_RENDERER).renderer(); + } } return renderer; @@ -197,6 +195,7 @@ private static void addBuiltInSources(final Context context) { addTileSource("OpenCycleMap (shows hills)", OPENCYCLEMAP); addTileSource("OpenStreetMap default style", OPENSTREETMAP); addTileSource("Ordnance Survey OpenData", OSMAP); + addTileSource("Offline Vector Maps", MAPSFORGE); builtInsAdded_ = true; } diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.java deleted file mode 100644 index 7a758e11b..000000000 --- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.java +++ /dev/null @@ -1,99 +0,0 @@ -package net.cyclestreets.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Environment; - -import static android.Manifest.permission.READ_EXTERNAL_STORAGE; - -public class MapPack -{ - private static final String MAPSFORGE_FILE_FORMAT_VERSION = "0.4.0"; - - public static void searchGooglePlay(final Context context) { - final Intent play = new Intent(Intent.ACTION_VIEW); - play.setData(Uri.parse("market://search?q=net.cyclestreets")); - context.startActivity(play); - } - - public static List availableMapPacks(Context context) { - final List packs = new ArrayList<>(); - - if (!PermissionsKt.hasPermission(context, READ_EXTERNAL_STORAGE)) - return packs; - - final File obbDir = new File(Environment.getExternalStorageDirectory(), "Android/obb"); - if (!obbDir.exists()) - return packs; - - for (final File mapDir : obbDir.listFiles(new CycleStreetsMapFilter())) { - final File map = findMapFile(mapDir, "main."); - final Properties props = mapProperties(mapDir); - final String name = props.getProperty("title"); - final String version = props.getProperty("version"); - if (map == null || name == null) - continue; - - packs.add(new MapPack(name, version, map)); - } - - return packs; - } - - public static MapPack findByPackage(final Context context, final String packageName) { - for (final MapPack pack : availableMapPacks(context)) - if (pack.path().contains(packageName)) - return pack; - return null; - } - - private static File findMapFile(final File mapDir, final String prefix) { - for (final File c : mapDir.listFiles()) - if (c.getName().startsWith(prefix)) - return c; - return null; - } - - private static Properties mapProperties(final File mapDir) { - final Properties details = new Properties(); - try { - final File detailsFile = findMapFile(mapDir, "patch."); - details.load(new FileInputStream(detailsFile)); - } - catch (IOException | RuntimeException e) { - } - return details; - } - - private static class CycleStreetsMapFilter implements FilenameFilter { - public boolean accept(final File dir, final String name) { - return name.contains("net.cyclestreets.maps"); - } - } - - ////////////////////////////////////////////////////// - private final String name_; - private final String path_; - private final String version_; - - private MapPack(final String n, - final String v, - final File p) { - name_ = n; - path_ = p.getAbsolutePath(); - version_ = v; - } - - public String name() { return name_; } - public String path() { return path_; } - public boolean current() { return MAPSFORGE_FILE_FORMAT_VERSION.equals(version_); } -} diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.kt new file mode 100644 index 000000000..667e38de6 --- /dev/null +++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/util/MapPack.kt @@ -0,0 +1,49 @@ +package net.cyclestreets.util + +import android.app.DownloadManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import net.cyclestreets.api.Maps +import net.cyclestreets.api.VectorMap +import java.io.File +import java.io.FileInputStream +import java.io.FilenameFilter +import java.io.IOException +import java.util.* + +class MapPack private constructor( + private val vectorMap: VectorMap, + context: Context +) { + val id get() = vectorMap.id + val title get() = "${vectorMap.name}${if(downloaded) "" else " (Needs download)"}" + val path = File(context.getExternalFilesDir(null), "${vectorMap.id}.map").absolutePath + val downloaded get() = File(path).exists() + + fun download(context: Context) { + val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request( + Uri.parse(vectorMap.url) + ) + request.setTitle("${vectorMap.name} Map Pack") + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + request.setDestinationInExternalFilesDir(context, null, "${vectorMap.id}.map") + + dm.enqueue(request) + } // download + + companion object { + fun availableMapPacks(context: Context): List { + val maps = Maps.get() ?: return emptyList() + + return maps.map { m -> MapPack(m, context) } + } + + @JvmStatic + fun findById(context: Context, packId: String): MapPack? { + return availableMapPacks(context).find { it.id == packId } + } + } +} \ No newline at end of file diff --git a/libraries/cyclestreets-view/src/main/res/xml/prefs.xml b/libraries/cyclestreets-view/src/main/res/xml/prefs.xml index f94e417d4..55091dc8e 100644 --- a/libraries/cyclestreets-view/src/main/res/xml/prefs.xml +++ b/libraries/cyclestreets-view/src/main/res/xml/prefs.xml @@ -9,6 +9,9 @@ android:title="Map style" android:summary="OpenStreetMap" android:defaultValue="CycleStreets-OSM" /> +