-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GPX import functionality #5369
base: master
Are you sure you want to change the base?
Conversation
Continuing from #5345. (#3898 is related and a use case for - persistently - showing the GPX track. I guess.) Regarding the persistence: Ha, that's a trap to fall into with Android. Saving instance state is actually not persistent - it would be gone latest when you restart your phone or exit the app (i.e. remove from recents). The track would need to be kept in a Rate limits Download distance UI Or, taking one step back from a concrete suggestion, it is just important to tell the user what is going to happen. In the current version, it is not clear what is going to happen if you uncheck the download-checkbox. |
@Helium314 |
persistence UI
|
JSON is too verbose. You could just save it in a comma-separated string and then "parse" it again from that. E.g. the string could be "lat1,lon1;lat2,lon2;lat3,lon3;...". You mention switches, but I mean radio button. Does the same apply there? There are plenty of quests where there are a couple of radio button and the answer (of course) only gets applied after one taps the ok button. In any case, is there a use case for wanting to download the route, but not display the route? I fear I am getting into the nitpicking phase, but maybe it would look better than this huge spinner if the distance would be a number input within the label? I.e. (o) Download data [ ] meters around track Oh, one thing came to my mind. US-Americans. You know, foot, inches, yards... hm. |
This is about the background map, it has nothing to do with quests. When doing many successive downloads to pre-load an area, which essentially is a manual version of what this PR does, I sometimes notice missing map tiles. Answering quests with a missing background map is still possible, but often it's quite hard to identify the element the quest is asking for. |
@Helium314 But what should be done if the download fails for one tile? I mean, it could only fail because the server from which it is downloaded for one reason or another doesn't return it (in time). Certainly, the whole download should not be cancelled for that. So, I am not really sure what we can do in that case, other than ignore it. @greuters You can do this (kind of pseudo-code):
So this does change the unit according to the location of the GPX track... maybe it would be better to change the unit according to the user's locale? |
Retry some limited number of times? |
@westnordost Nice, that sounds like a good solution for the Locale, I'm gonna try that. To ensure all tiles are downloaded like @Helium314 suggests, I think a combination of using a retryLimit and a flag ensureTilesDownloadSuccess in Downloader.download could be a sensible way. If mapTilesDownloader.download reports any failed tiles, it could be started over a number of times (e.g. to recover from intermittent connection problems). If it fails too often, a ConnectionException or similar could be thrown which triggers an error shown to the user. Maybe a toast is not appropriate though, some message might be better because these downloads can take quite a while and the user probably doesn't observe his phone all the time. At least this way the user would know that some downloads failed, and could also choose to restart the whole import manually. |
That luckily can be kludged by translation alone in this particular case! 😃
|
Hmm, I think this does not hold for the confirmation dialog though - probably the appropriate conversion would be to acres -> 1 km^2 = 247,105 acres |
does the confirmation dialog need the total area, though? I would think that number of areas to download would be indicative enough to allow user to choose whether to proceed or not. |
Yes, I'd say some indication is needed. It is basically a tradeoff between number of downloads and download size - if you set the download distance to high values, you max out on the area you are downloading. One option might be to show the number of unique tiles instead, but that is less intuitive for users I guess. Probably lifting to API level 24 is not around the corner? This would provide a nice MeasureUnit type.. I think I will stick to a simple proposal for the moment though. |
That was my idea, and possibly inform the user if some tiles fail after the retry limit. Actually, cache size and average tile size could be used to determine whether downloading all tiles makes sense at all, i.e. everything fits in cache. Sorry for always pushing background map things, but I encountered missing map tiles too frequently, and they are really annoying (and will cause users to open issues). |
While the topic of retrying downloading tiles is certainly a useful issue to solve, I do not think it is integral to this PR. It could be a separate PR. On MapLibre, my last information on it is that an experimental implementation has been done but you are concerned about performance and that you want to wait on some API change in an upcoming major version before continuing, while enocouraging that someone else could already port the StreetComplete style to MapLibre. (For the latter, I think there could be a ticket with "help wanted", but only if we know that the port will actually be done, otherwise whoever does that port might end up feeling cheated, i.e. invested time for nothing) Another thing that was mentioned was an enhancement of the sync-notification on Android, adding a "cancel" button to stop a (too large) download. While this would be very nice, especially with the download times and sizes we are talking about here, I too think that this is not essential to this PR and could be a separate PR. I'll have a more detailed look over the PR today and add some comments about the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not had a detailed look at the actual GpxImporter
/ GpxTrackSampler
yet, however, from looking at it generally, I see there are some issues with this. Rather than to tell you the issues, I will outline how it is supposed to be:
A Sequence
in Kotlin is what a Stream
is in Java, i.e. lazily evaluated iterable. This is very handy for when you have, say, something like a parser, which emits data classes from a continuous input stream. E.g. the difference between
fun parseGpx(): List<Trackpoint>
and
fun parseGpx(): Sequence<Trackpoint>
is that the former would load the entire list of trackpoints into memory first (consumes the input stream till the end), while the latter only consumes it when the sequence itself is consumed, i.e. turned into a list. Very handy when handling large data, but not important when handling small data. See https://kotlinlang.org/docs/sequences.html for a better explanation.
A Flow
is basically a "coroutine-enabled" Sequence
. I believe this is what you would like to have. Though I have to admit, I did not look properly into Flow
s yet and it is also not used anywhere in the app yet, so if you want to stay on terrain where I can actually help you, you could first do it with sequences and only later use flows instead. (The API should be similar). Docs: https://kotlinlang.org/docs/flow.html
So, the parsing should look like this:
-
Do not use a parser class that is part of a third party library. The parser is specific to its use case and also only parses GPX track as outputted by the OSM API. Other GPX can be (a little) more complex, more nested. Also, it's non-flow/sequence aware Java-code, to retrofit that into this kind of code is making things needlessly complex
-
The GPX parser emits a
Flow
of trackpoints from an input stream, nothing more. It can be tested in isolation.fun parseGpx(inputStream: InputStream): Flow<Trackpoint>
. And since it's actually possiblyFlow<Track>
withTrack
itself consisting of a flow ofTrackSegments
, each of which also consists of a flow ofTrackPoint
s according to the GPX schema definition
, it's fair enough that the function does not do anything more, as it is more enough (to test) -
The flow can then be further processed, functional style. E.g. in kinda pseudo code (assuming a sequence, because that's what I am used to):
val allTheSampledTrackpoints: Sequence<Sequence<Sequence<Trackpoint>>> = parseGpx(in)
.map { track ->
track.map { segment ->
val lastPoint = null
segment.flatMap { point ->
if (lastPoint == null) sequenceOf(point)
else sampleAtMaxDistanceOf(maxDistance, lastPoint, point).toSequence()
lastPoint = point
}
}
}
where sampleAtMaxDistanceOf
is a function that returns a list of trackpoints in between and including the two passed points where each point is maxDistance or less apart. This function can also be tested in isolation.
- In the next step, the sequence (of sequence of sequence, as per the nested structure of a gpx) can be
.flatMap
ed to tile positions. Alternatively, this could be done without this step in-between, I am just mirroring in this example how I understand is your data flow.
app/src/main/java/de/westnordost/streetcomplete/data/import/GpxImporter.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/de/westnordost/streetcomplete/data/import/GpxImporter.kt
Outdated
Show resolved
Hide resolved
...main/java/de/westnordost/streetcomplete/screens/main/controls/GpxImportConfirmationDialog.kt
Outdated
Show resolved
Hide resolved
...main/java/de/westnordost/streetcomplete/screens/main/controls/GpxImportConfirmationDialog.kt
Outdated
Show resolved
Hide resolved
...main/java/de/westnordost/streetcomplete/screens/main/controls/GpxImportConfirmationDialog.kt
Outdated
Show resolved
Hide resolved
private val inputStream: InputStream, | ||
private val callback: (result: Result<GpxImporter.GpxImportData>) -> Unit, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fear this will not work. Unfortunately. You cannot pass any parameters to any Fragment
in the constructor. The reason is that the Android system is allowed to destroy fragments and activities at any point (e.g. when you tab out and in again) and re-create them (with empty constructor) later.
I think in general the whole setup of deferring is incorrect / "not the kotlin way", which just makes the whole thing more complex as it needs to be. The UI should be clearly separated from the data (flow). See my general comment for more details.
Sorry to ask you to basically re-implement how you laid out the code, better now than later, though, since you didn't write tests yet. I suggest you leave the UI be for now and focus on the data first - parsing, processing and each unit testing these in isolation. That should give you a stable base to work with when integrating it into the UI. |
Thanks for looking into my code and giving detailed feedback. I addressed the small issues in a commit and will have a go at restructuring the importer with sequences, just one question:
Should I just avoid using the GpxTrackParser, or do you mean to drop using an XML library at all? XmlPullParserFactory seems to be one of the recommended choices for Android according to https://developer.android.com/develop/connectivity/network-ops/xml To be able to display the track we will anyway need to keep a full list of all original trackpoints in memory I think, so maybe it would make sense to just load this list with a custom GpxImportParser (extends XmlParser) and then continue from this list.asSequence() to do the whole resampling? |
I meant just the
Mh, not sure if I understand you correctly. You have a point that the whole list of points needs to be kept in memory anyway. However, returning a sequence (and maybe a flow) is not actually harder to do than returning a list. May be even something like 1-2 lines less code, actually. And as you (and me) would like the loading to be cancelled if the UI for it is destroyed, at the end, a flow seems to be what we want. Anyway, do not extend some generic parser. Note how also the code example in the link you posted does not extend a XmlParser, instead, it uses the parser. As Java devs, we are maybe used to inherit in order to inherit functionality (for code sharing), but it is in fact not such a good idea. |
Hm actually, does the import need to be cancelled if the dialog is closed? The whole thing - load gpx, sample gpx, determine to-be-downloaded tiles could actually be part of the downloading, i.e. is being done in the background, couldn't it? Then the confirmation step would be omitted, but it could be replaced by showing an actual progress bar in the download notification and a button to cancel it. But anyway, also the download that happens in the background should be cancellable, one way or another. |
Ok, I think that is something I can work with. So I'll only use the XmlPullParser and work on sequences for the moment (changing for flow in a next step). I'm coming from Java, so yes, this kind of stuff needs a bit of rethinking and learning on my side ;) |
Great to hear! I am also coming from Java, and the first time I read the examples in https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/sequence.html ... I was like... "whoa! What??" Well, I hope you share my enthusiasm for that! |
I started changing my backend code to use sequences and get rid of parser dependencies, but didn't get so far yet. As our stationary time in Puerto Escondido is coming to an end, I don't know when I will continue with this work exactly. Latest by end of February, when we will take a while off from bicycle touring once again in Arizona. So I just wanted to share my intermediate state and keep you updated of my plans. @westnordost can you have a glance at GpxImportKt#importGpx some time, is this more or less the approach you had in mind? Of course the interface to the UI still has to be adapted as well, and I did not come up with a good solution to safely open / close the file and return a Sequence yet, hence parseSingleGpxTrack(inputStream: InputStream) still returns a list. |
No problem, this PR can stay as a draft here for a while.
Uf, I wouldn't do that. Because of this, my pseudo code earlier mentioned stuff like Otherwise, it looks about right. Regarding
Hm, right. Well, wouldn't the solution be to (not use The overall structure of it is exactly what I had in mind 👍 . The singular mapping functions should also be testable in isolation (this way). Looking at the test, it seems to be unnecessarily complicated to me. Let's assume that the Android XML parser behaves correctly and instead of mocking it, just test the parser with actual XML strings. (If necessary, the test needs to be moved to |
059de18
to
86d3703
Compare
I finally got around to put some more work into this. The current state is as
How would you like to have the UI? Should there be a 'Downloading' dialog showing progress, any error/success messages and have a Cancel button? Maybe if we do this we could skip the confirmation dialog, as the user can simply abort if it takes to long download. |
Cool, congratulations! Unfortunately I don't have too much time right now and your PR is quite a mouthful so I can't review it soon in depth, maybe someone else would also review this? A few remarks from reading this vertically:
|
Regarding the UI, your last screenshot looks nice. I think I didn't criticize it, right? |
12b4787
to
ca11ace
Compare
Nice, thanks for your thoughts. Regarding the UI, I thought about these comments:
but if you are good with the way it is right now, I would just keep it that way for the time being. Thinking about @Helium314 's valid comments regarding missing tiles, I guess that would be a good improvement to come (actually keeping a dialog open while downloading, showing progress and an error message if tile download was not entirely successful in the end even with retries). So to summarize, I'll be working on the following for now:
|
I think you don't need to bother any more, as this is handled differently when #5693 is merged. |
|
To map along a predefined route, set your cache big enough and export a GPX track from some other routing app by sharing it with StreetComplete. StreetComplete imports the track to show it on the map, and - if you accept the number of downloads and area - schedules tile downloading along the track for offline usage,
ca11ace
to
0c701fd
Compare
The main screen has been migrated to ViewModels + Compose now. This means that the UI, i.e. the dialog, would need to be redone. IIRC the loading of a GPX track is done via an Android Intent, right? See |
I noticed only now that you force-pushed this two months ago and that indeed the xml parsing is based on the xmlutil now, plus, the rendering of the track is based on maplibre now. Tell me if anything (any part) should be reviewed. Edit: typo, I meant reviewed, not removed. |
To map along a predefined route, set your cache big enough and export a GPX track from some other routing app by sharing it with StreetComplete.
StreetComplete imports the track to show it on the map, and - if you accept the number of downloads and area - schedules tile downloading along the track for offline usage,
Fixes #473, fixes #1757.