How To Find Things
The size of the code might be daunting (it is for me, the author), so this section is intended to help with finding, where exactly to contribute when you want to change something.
Note that the code is in a constant (but glacial) flux, so some of those might be possible to change with a plugin in the future.
Editing Standard Fields
As mentioned in the plugin docs, "standard fields" is a third group of fields that gets added on top of "fields" and "more fields" for every object in the editor panel. This chapter traces its addition, marking what and where can be improved.
OsmChange
First we must mention that the single most important class in the app is
OsmChange
.
It defines a set of changes over the base OsmElement
received from OpenStreetMap,
and a few methods to manipulate an object. For example, it acts as a Map<String, String>
for tags, but keeps the changes separate.
It can calculate an age based on the check_date
tag (but note that it's a bit inconsistent with the overridden values for the
amenity
mode). There are methods to confirm an object (check()
) and to toggle
its disused status. The one most important method that's used everywhere returns
full tags in a map: getFullTags()
. It is of course cached.
Detecting a Preset
Now, to learn whether we need to add and shuffle fields, we first need to detect
a preset. Let's look at PoiEditorPage
's state. In the initState()
, the amenity
(the object we edit) is either copied or created anew, and
updatePreset()
is called. For an already existing object (or a newly created from an NSI preset),
we call getPresetForTags()
from a preset provider.
That method has got a lot of steps: checking for empty tags and amenity=fixme
tags,
asks plugins for an answer. But the main step is running a huge SQL query against
the local SQLite database, which through clever CTE and sorting returns the one result.
Is it good to have a single big SQL query? Felt like a clever idea at the time, but now I'm not so sure. But this method gets called dozens of times per second in the micromapping mode, so this could be considered optimizing.
Localization
All presets and fields are translated, so we pass the current Locale
to preset
provider methods. The plugin subsystem too requires it — and that's basically it.
App localization is handled by the intl
package.
Passing locales to SQL queries in the preset provider works relatively simple.
We create a CTE
for a langs
table that contains two columns, language
code and a rank, starting with 1. And we join the results with this table,
ordered by the language rank. Since window functions do not work in some
SQLite versions, we just query all the results and keep the first one for
each preset or field.
Querying Fields
After we have a preset, we call getFields()
Initially a preset is returned with empty fields, because its name and title
can be all we need (e.g. for the micromapping mode). But for the editor, we need
the fields. And we do another SQL query to the presets database,
querying the fields
table with the list from the preset_fields
.
Besides checking for a location, sorting languages, and sorting into two
lists ("fields" and "more fields", based on the required
flag), it does two
more things.
First is building the combo options. While needed only for combo fields, it is a very resource-intensive operation, since it conflates three sources:
- Options listed in the field definition.
- Popular tag values from TagInfo.
- Popular tag values from all the downloaded data.
The result is stored in a database table, to not do this again. But still, it is another database query, so make it four for cache misses.
That's why the getFields()
method implements its own in-memory caching. You
can notice how long the first loading of the editor pane takes. That's because
in the collapsed "more fields" section there are dozens of combo fields.
Finally, the method calls the long fieldFromJson
function from
field.dart
.
It has some overrides for common keys, and for others, it uses the type.
It is pretty straightforward. When you're adding a field, do not forget
to create it twice in this file, in both functions.
Built-in Fields
For some OSM keys (not preset names, and not field types) there are specific
classes instantiated in the fieldFromJson()
. For example, NamePresetField
for a name, or a WebsiteField
for a website. Those provide unique experience
in Every Door, often with shortcuts to edit tags faster, or set multiple tags
at once considering the context.
Postcode and Opening Hours
Note that field processing (this section, standard fields etc) happens only when the object "needs address". Which means, it's either an amenity, a building, or an address point.
First thing, an opening hours field is added unconditionally. Things just tend to have those.
Then, a postal code (zip code) field is added. It is partially a hack: the address field in the default presets includes everything, but is not used. Instead, a custom field is added in Every Door, but it does not set a postcode (because it's a building property, not an amenity's). But some mappers might need it. For buildings and address points it is added to the main fields list, for other objects — to the additional list.
Standard Fields
An amenity is considered to need the full standard fields set when its preset mentions
both opening_hours
and phone
anywhere. Otherwise a shorter list of just two
fields is added, for an address and a floor.
You can find the list of fields in
kStandardPoiFields
.
Note that it is constant, not dependent on anything. Each field is constructed
alike to the getFields()
method: queried from the preset database, injected
with combo options, and cached. Plugins of course contribute to the results.
When we don't need the full set, but a preset mentions some fields from the list, they are moved to the standard fields section as well.
Sorting Fields
At this point, we have three lists of PresetField
instances:
pre-filled stdFields
, and empty fields
and moreFields
.
The extractFields()
method copies field instances from the preset to the latter two lists.
With two exceptions: first, the standard fields are obviously skipped.
Second, any fields from the moreFields
are moved to fields
if their
key is present on the object, or they have a matching
prerequisite.
After this we have all three list prepared, and can re-render the editor page, displaying them to the user.
Zooming Out
Every Door uses flutter_map library for its mapping. Over the years it's been doing pretty complex things with it (but not "going native" complex). This section is mostly about providers and components: what happens when you change modes implicitly.
Map Controllers
Flutter map instances report their state and can be controller via MapController
objects.
Note that when you use it for yourself, there are
certain troubles
you have to go through.
When you use the CustomMap
widget in Every Door code, for bi-directional conversation
you would pass the CustomMapController
, which gets connected to the internal fields
of the widget. With it, you can:
- Access the
MapController
itself, so that you can access and affect the map state from outer listeners. - Access the
GlobalKey
for theFlutterMap
widget. It is used once, for detecting its geometry on the screen. - Zoom the map to a list of locations. Useful for amenities and micromapping modes.
In the CustomMap
itself, there is a lengthy MapEvent
listener. It only processes
user-initiated events, and does this:
- After moving the map, it updates the center location in
effectiveLocationProvider
. - After rotating, it updated the angle in
rotationProvider
(the value is commonly used forinitialRotation
property of theFlutterMap
constructor). - During the map movement initiated by the user, it disables GPS tracking
(in
trackingProvider
), and updates the zoom level inzoomProvider
.
And when the switchToNavigate
flag is set, it also controls the navigation mode switch.
Modes and the Browser
Interactivity
Decisions on zoom levels and interactivity
Uploading to OpenStreetMap
This section traces what happens when a user taps the upload button, but does not explain the entire uploading algorithm: it's a bit too complex. It would be useful to you to understand how providers interact, and how the data is packaged.
This section will be completed later.
- Nav panel, upload button
- Upload provider
- Locking
- Uploading OSM changes
- Splitting changes
- Downloading data
- Uploading data
- Updating the inner storage
- Uploading notes
- Uploading scribbles
- Notification