Skip to content
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

Expose relation membership to schema configs #351

Open
Tracked by #247
msbarry opened this issue Sep 25, 2022 · 9 comments
Open
Tracked by #247

Expose relation membership to schema configs #351

msbarry opened this issue Sep 25, 2022 · 9 comments
Labels
config YAML config functionality

Comments

@msbarry
Copy link
Contributor

msbarry commented Sep 25, 2022

Needed for shortbread boundaries layer

@msbarry msbarry changed the title Expose relation membership to schema configs (shortbread boundaries layer) Expose relation membership to schema configs Sep 25, 2022
@msbarry msbarry added the config YAML config functionality label Sep 25, 2022
@quincylvania
Copy link
Contributor

Greetings! Thanks again for your work. I think this is the blocker for a few issues over at osmus/OpenTrailMap. A lot of OSM trail data we want to use, such as membership and hierarchy, are tagged on route relations instead of individual ways. We don't want to import route relations as wholesale objects, we just want to query relations when filtering ways and also copy over some of their tags.

I'd be happy to discuss a schema implementation or even work on it myself if you can point me in the right direction.

@msbarry
Copy link
Contributor Author

msbarry commented Jul 23, 2024

Awesome! Let's figure out what the yaml schemas should look like first then work backwards... What would you want the yaml schemas to look like for those use-cases you linked?

@quincylvania
Copy link
Contributor

I think something simple like having a keyword to navigate up the parent relation would work, plus a keyword to specify the role of the child feature.

Given that features can have any number of different, unrelated parent relations, the feature should match if any parent relation matches. However, for convenience, a parent relation must match all statements in the list to be considered matching.

Example: match all ways that are either cycleways or are part of a bike route relation:

geometry: line
include_when:
  - highway: cycleway
  - __parent_rel__:
    - type: route 
    - route: bicycle

Example: match all ways that are part of a bike route OR part of a hiking route:

geometry: line
include_when:
  - __parent_rel__:
    - type: route 
    - route:
      - bicycle
      - hiking

Example: match all ways that are part of a bike route AND part of a hiking route:

geometry: line
include_when:
  - __all__
    - __parent_rel__:
      - type: route 
      - route: bicycle
    - __parent_rel__:
      - type: route 
      - route: hiking

Example: match all the nodes that are labels of admin boundaries:

geometry: point
include_when:
  __parent_rel__:
    - type: boundary
    - boundary: administrative 
    - __role__: label

Example: match all waterway lines that have a main_stream or side_stream role in a waterway relation:

geometry: line
include_when:
  - __parent_rel__:
    - type: waterway
    - __role__:
      - main_stream
      - side_stream

Example: match all waterway lines that are segments of waterway relations, excluding tributaries:

geometry: line
include_when:
  - __parent_rel__:
    - type: waterway
    - __not__:
      - __role__: tributary

In the future this syntax could also work for navigating from nodes up to ways (__parent_way__), and possibly navigating down (__child_mem__, __child_node__).

For attributes, I think just having the above functionality in the attribute include_when and exclude_when statements is enough for now. While it might be nice to copy attributes from parent relations, this would bring situations where multiple matching parent relations have different tag values for the same key.

Example: add is_nwn=true to all ways that are part of one or more hiking routes with network=nwn:

geometry: line
include_when:
  __parent_rel__:
    - route: hiking
attributes:
  -
    include_when:
      - __parent_rel__:
        - route: hiking
        - network: nwn
    key: is_nwn
    value: true

Note the duplicated route: hiking. This is needed since the top level include_when filters the ways but does not filter the parent relations. As such, the include_when under the attributes can examine all parent relations, including those that might not be relevant.

Example: add is_nwn_only=true to all ways that are part of one or more hiking routes with network=nwn and are not part of any hiking routes with a different network value:

geometry: line
include_when:
  __parent_rel__:
    - route: hiking
attributes:
  -
    include_when:
      - __parent_rel__:
        - route: hiking
        - network: nwn
    exclude_when:
      - __parent_rel__:
        - route: hiking
        - __not__:
          - network: nwn
    key: is_nwn_only
    value: true

@msbarry
Copy link
Contributor Author

msbarry commented Jul 30, 2024

Thanks! This looks like reasonably simple approach to navigate the connectivity graph. The other functionality available to us is expressions, so something like - "${feature.parent_relation.route == 'hiking' && feature.parent_relation.network == 'nwn'}" although the nice thing about the way you've modeled it is that a preprocessor can inspect all the tags used for filtering or copying through to the output so it can limit what needs to be stored in memory.

A few other use-cases I'd want to think through how to handle:

  • superrelations: how might you express when the parent of the parent relation matches, or when an nth-level parent relation matches?
  • treating relation as a single geometry: In [FEATURE] OSM multiline relation handling #857 we discuss options for how to treat a relation containing many ways as a single multilinestring - could that fit into this at all? Or is it completely separate functionality you'd express a different way?
  • getting attributes from parent relations: You mention "While it might be nice to copy attributes from parent relations, this would bring situations where multiple matching parent relations have different tag values for the same key." - this comes up for boundaries where a way might belong to the boundary for multiple admin levels, and you want to set the admin level on the line to the minimum admin level of a relation it's contained in. I'd be curious how we might be able to select attributes from a parent relation that come from different include_when filters, or for multiple relations matching a single include_when

@quincylvania
Copy link
Contributor

- "${feature.parent_relation.route == 'hiking' && feature.parent_relation.network == 'nwn'}"

The problem with this syntax is that it's not clear if you're querying one parent relation or all parent relations. Does it return true if the feature has two parents, one with only route=hiking and another with only network=nwn?

For superrelations, I think each __parent_rel__ statement should only query the direct parents. We can nest __parent_rel__ statements if we want to query parents of parents. Generally any data needed for rendering will just be one or two levels deep, in a predictable format.

include_when:
  - __parent_rel__:
    - type: route
    - __any__
      - network: nwn
      - __parent_rel__:
        - type: route
        - network: nwn 

Concerning #857, I think that just has to do with the geometry type. I consider it a separate issue. (Though it could benefit from using a similar syntax like __child_member__).

@quincylvania
Copy link
Contributor

I'd be curious how we might be able to select attributes from a parent relation that come from different include_when filters, or for multiple relations matching a single include_when

I guess people would want to copy over attributes pretty often… I think expression syntax would actually work okay if there were variables for dealing with multiple parent tag values.

# return all values as a semicolon-separated list (unsorted)
value: '${ feature.parent_relations.tags.admin_level }' # 4;2;4;6;6

# return one of the values if present (we don't care which one)
value: '${ feature.parent_relations.tags.admin_level.any }' # 4

# return the lowest numeric value (or lowest string value if not all numbers)
value: '${ feature.parent_relations.tags.admin_level.min }' # 2

# return the highest numeric value (or highest string value if not all numbers)
value: '${ feature.parent_relations.tags.admin_level.max }' # 6

# return the most common value, or in the case of a tie, a semicolon-separated list (unsorted)
value: '${ feature.parent_relations.tags.admin_level.mode }' # 4;6

# return one of the most common values (we don't care which one)
value: '${ feature.parent_relations.tags.admin_level.mode.any }' # 6

We might also want to copy over some other parent metadata:

# return all parent relation IDs as a semicolon-separated list (unsorted)
value: '${ feature.parent_relations.id }' # 24812839;559392

Note that without another property there would be no way to filter the parent relations prior to querying the attributes (which could be a problem in cases where different sorts of parent relations use the same tags for different things).

@msbarry
Copy link
Contributor Author

msbarry commented Aug 2, 2024

For superrelations, I think each __parent_rel__ statement should only query the direct parents. We can nest __parent_rel__ statements if we want to query parents of parents. Generally any data needed for rendering will just be one or two levels deep, in a predictable format.

👍 that's good to know, it definitely simplifies things if we don't need to support arbitrary levels of nesting.

- "${feature.parent_relation.route == 'hiking' && feature.parent_relation.network == 'nwn'}"

The problem with this syntax is that it's not clear if you're querying one parent relation or all parent relations. Does it return true if the feature has two parents, one with only route=hiking and another with only network=nwn?

It would probably be more code-like with CEL map/filter/exists macros like ${feature.parent_relations.exists(rel, rel.tags.route == 'hiking' && rel.tags.network == 'nwn')}. We'll probably need to make parent_relations available as an attribute on the feature for expressions, independent of the yaml syntax for it - but I don't think we'd want to rely only on expression syntax.

Using those macros I think we'd be able to express the min admin level like: ${math.least(feature.parent_relations.filter(rel, rel.type == 'boundary').map(rel, rel.tags.admin_level))}

@quincylvania
Copy link
Contributor

quincylvania commented Aug 5, 2024

It would probably be more code-like with CEL map/filter/exists macros like ${feature.parent_relations.exists(rel, rel.tags.route == 'hiking' && rel.tags.network == 'nwn')}.

This looks great. I'm not familiar with the ins and outs of CEL, but advanced code-like syntax is exactly what we need for something this complex.

@ZeLonewolf
Copy link
Contributor

I would like to render boundary relations as polygons, and I'm willing to work on the code to make that happen. If we're happy with the syntax hashed out above (thanks for that!) I'd like to work on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
config YAML config functionality
Projects
None yet
Development

No branches or pull requests

3 participants