OpenStreetMap logo OpenStreetMap

Consuming conditional access tag values

Posted by Hungerburg on 5 December 2021 in English. Last updated on 21 October 2022.

During the summer, a number of wild-life sanctuaries found their way into the openstreetmap data around the area of my local knowledge. Legal provisions place a simple conditional restriction on accessing the protected areas: From November 16 up to April 30 one must not stray from tracks, well-known paths and pistes. Some days ago, I started to think about ways to add that to the data, and came up with access:conditional=only_trail_use @ (Nov 16-Apr 30). That felt pretty good human readable, Apart from a new restriction, it was very much in line with what the documentation on conditional restrictions proposes. The new value itself follows established naming conventions of documented restrictions.

Still, that seemed too little, to pitch this tagging to a consumer of openstreetmap data in close vicinity, developing an online slippy map, that sets out to highlight protected areas to the interested public, i.e. skiers and other winter sport enthusiasts. I searched for a reference implementation, that parses openstreetmap conditional restrictions, and I did not find a single one.

So I had to start from scratch: Below some lines of python, that take the (very simple) tag value from above, and find out, if the restriction applies at a certain date. In all its simplicity, there are some pitfalls worked around. For more complete coverage, it might still be wise to look for a full blown calendaring module to evaluate time constraints, and of course a full blown parser of the myriad of expressions available in conditionals.

import re
from datetime import datetime
from calendar import monthrange

day = '((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)(?: +[0-9]{1,2})?)'
rex = re.compile('(\S+) +@ +\(' + day + ' *\- *' + day + '\)')
now = datetime.now()
dbg = True
dbg = False

def firstof(date):
	if re.findall('[0-9]+', date): return date
	return date + " 1"

def lastof(date):
	if re.findall('[0-9]+', date): return date
	month = datetime.strptime(date, "%b").month
	return date + " " + str(monthrange(now.year, month)[1])

def applies(conditional, checkpoint):
	m = rex.match(conditional)
	what = m.group(1)
	tfrm = datetime.strptime(firstof(m.group(2)) + " " + str(now.year), "%b %d %Y")
	tend = datetime.strptime(lastof(m.group(3)) + " " + str(now.year), "%b %d %Y")
	tchk = datetime.strptime(checkpoint + " " + str(now.year), "%b %d %Y") # Feb 29 !
	if dbg: print(">", tfrm.date(), "", tend.date(), "?", tchk.date())

	if tchk == tfrm or tchk == tend: return "" + what + " @ " + checkpoint

	if tchk < tfrm: tchk = tchk.replace(year=now.year+1)
	if tend < tfrm: tend = tend.replace(year=now.year+1)
	if dbg: print("<", tfrm.date(), "", tend.date(), "?", tchk.date())

	if tchk > tfrm and tchk < tend: return "" + what + " @ " + checkpoint
	return "" + what + " @ " + checkpoint

if __name__ == "__main__":
	today = now.strftime("%b %d")
	print(applies("discouraged    @ (Sep  1-Dec  4)", today))
	print(applies("only_trail_use @ (Dec 24-Dec 23)", today))
	winter = "Jan 15"
	print(applies("no  @ (Sep 1 - Jan 6)", winter))
	print(applies("yes @ (Nov 16-Apr 30)", winter))
	sometimes = "Jun 30"
	print(applies("discouraged @ (Mar-Jun)", sometimes))
	sometimes = "Mar 1"
	print(applies("only_trail_use @ (Mar 3-Jun)", sometimes))

PS: The year is only added to the terms, because otherwise checking on February 29 in a leap year might cause an exception in the datetime module. Exceptions are not checked at all, but this is not production code. January 15 gets used as a reference date to see, if the condition applies in winter, because then, winter should have arrived. More use cases: Conditionally show markings on a vector map or display a prompt as a geofencing hint in an app. Update 22-10-22 - complete dates where only a month is given (firstof/lastof).

Discussion

Comment from SimonPoole on 6 December 2021 at 12:20

https://github.com/simonpoole/ConditionalRestrictionParser has been available and in use for half a decade.

I wouldn’t suggest that it’s a ‘reference implementation’ as the tag itself is woefully under specified and many things about it are not well thought out.

Comment from Hungerburg on 6 December 2021 at 21:40

Cool, A real parser in the .jj file. I guess from coercing openstreetmap restrictions into BNF or some such formalism, a number of proposals can spring from. Sadly my mind has deteriorated too much to be on any help, since writing a Bison parser for something much more simple years ago; and even then… Still, there are lots of smart people! Writing down the grammar might be a “good first issue” :)

Comment from Hungerburg on 5 February 2022 at 23:37

Late Update: The data - http://overpass-turbo.eu/s/1ell, The consumer (xctrails) already used Simon’s Lexxer/Parser on the server, before I wrote my exercise, but the work still was not in vain ;)

Comment from Arminus on 21 October 2022 at 12:34

BTW: Your regex doesn’t work for this pattern: “no @ (Mar-Jun)” - which is legal according to https://wiki.openstreetmap.org/wiki/Conditional_restrictions#Restriction-value

Comment from Hungerburg on 21 October 2022 at 22:04

Hallo Arminus, das ist wohl wahr! Die Zeichenkette heißt ja auch “day”, weil sie einen Tag will :) In der regex muss dann der Tag optional werden, und in Folge ein Tag eingesetzt werden, wo der fehlt. Ich werde das gleich oben ändern.

PS: Du mappst ja selber Steige und bereitest Daten für Karten auf? Fällt dir dazu etwas ein - https://wiki.openstreetmap.org/wiki/Proposed_features/highway=scramble - ich will das bald zur Abstimmung bringen und bin über jeden Rat froh.

Comment from Arminus on 22 October 2022 at 08:09

Danke für den Fix, ist eleganter als meiner gestern ;-) (hatte Deinen code für einen NSG importer dann doch verwendet).

Das Proposal kenn ich, falls mir ein sinnvoller und noch nicht diskutierter Beitrag einfällt, dann kipp ich das irgendwo ein.

Log in to leave a comment