For many years, the University of Tennessee Libraries has utilized GeoNames as a source for dereferencing and reconciling geographic location data related to our works. The GeoNames API allows consumers to retrieve many metadata elements about a place for free including "point" information in the form of latitude and longitude. The relative ease of adding geographic data has encouraged us to add this data to our MODS records at /mods/subject[geographic]/cartographics/coordinates near its corresponding string and reference to the source object in Geonames.

1
2
3
4
5
6
<mods:subject authority="geonames" valueURI="http://sws.geonames.org/4624443">
   <mods:geographic>Gatlinburg</mods:geographic>
   <mods:cartographics>
      <mods:coordinates>35.71453, -83.51189</mods:coordinates>
   </mods:cartographics>
</mods:subject>

While we historically mapped this data, we never actually leveraged it and used it for display or navigation. Recently, the Center for Digital Humanities at Saint Louis University released a new Beta viewer that leverages navPlace properties in IIIF manifests and displays them on a map. Depending on where the data is found within the manifest depends on how it functions in the viewer. Since we already had this data easily available, I decided to add code to add navPlace properties to our manifests via our IIIF Presentation v3 service.

The IIIF Presentation Specification does not provide a resource property designed specifically for geographic location. Because of this, the navPlace extension exists. The navPlace extension is not very prescriptive. The property is allowed in most classes defined by the IIIF presentation v3 specification. According to the extension, a navPlace property is allowed on a IIIF Collection, Manifest, Range, and / or Canvas.

Rather than adding the property everywhere, I decided to start small by adding the properties only to Manifests and Ranges. While one may argue that it makes most sense to add this information to the Collection, I would argue that the behavior of navPlace viewer makes this unnecessary. That's because the viewer attempts to dereference all id properties in search of other navPlace properties that may appear in embedded classes. Because of this, a Collection that references a Manifest with an navPlace property will render those if the Collection is passed to the viewer.

In the next few sections, I will describe our implementation, how the viewer consumes and makes use of them, and personal desires for future consuming applications.

Rendering navPlace Data in navPlace Viewer

The navPlace viewer provides different experiences for navPlace properties on the Manifest and the Range. When the property is placed on Manifest, the label of the Feature, the summary, and links to open the manifest in Universal Viewer and Mirador are associated with the marker associated with the geographic point. When a user clicks on the point, a bubble expands with this information prominent. According to the README, it should also render the thumbnail of the first canvas.

Display of navPlace on Manifest

Properties on a range are rendered differently. The label of the Feature is associated with the marker with a similar user experience. Unfortunately, links to the viewers aren't present.

Display of navPlace on Range

This makes sense. The range has an items property that includes a canvas that targets the primary canvas in the manifest with a temporal media fragment attached to the end. The URI for the part of the Canvas in the Range is not dereferenceable. In order for a viewer to make use of the range, it would need to convert the id to a content state URI and the viewer would need to dereference it. Unfortunately, navPlace viewer doesn't do this, and neither Univeral Viewer nor Mirador would dereference it currently if it did.

Content State and Desires for Consuming Applications

While the delivery of navPlace data on manifests in navPlace viewer meets our needs, the delivery of the property on range leaves a lot to be desired. The temporal data associated with the canvas inside the range is used to provide navigation to specific points in an audio or video work in viewers such as Universal Viewer and our own RFTA canopy. For instance, in our Rising from the Ashes oral history portal, the range data is leveraged to provide users a way to navigate to specific interview questions or geographic locations discussed within an oral history work. When a user clicks a temporal based part, it causes the viewer to update to that specific point.

Geographic Navigation in Becky Jackson Interview

In my opinion, this navigation experience would be better if it were a map with navigable points rather than the list you see above. Furthermore, this could be enhanced even further by allowing the map to represent an entire collection of manifests and rendering the navPlace properties on each. Then, users could start at the map and navigate to specific points in works without starting at the work.

In order for this to happen, this will require two things. First, the map viewer or initial consuming application must translate the range to a IIIF content state URI. Content state provides a way to refer to a IIIF Presentation API resource, or a part of a resource, in a compact format that can be used to initialize the view of that resource in any client. The id associated with the range is not intended to be passed to a viewer in the same form that its found in the manifest. Instead, it must be transformed into a content state URI. To do this, the viewer needs to take the range and convert it to an annotation body like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "@context": "http://iiif.io/api/presentation/3/context.json",
  "id": "https://digital.lib.utk.edu/assemble/content-states/1",
  "type": "Annotation",
  "motivation": ["contentState"],
  "target": {
    "id": "https://digital.lib.utk.edu/notderferenceable/assemble/manifest/rfta/8/range/places_mentioned/1",
    "type": "Range",
    "partOf": [
      {
        "id": "https://digital.lib.utk.edu/assemble/manifest/rfta/8",
        "type": "Manifest"
      }
    ]
  }
}

Then, this JSON needs to be encoded according to the IIIF Content State Specification to ensure it is not vulnerable to corruption. The specification defines a two step process for doing this that uses both the encodeURIComponent function available in web browsers, followed by Base 64 Encoding with URL and Filename Safe Alphabet (“base64url”) encoding, with padding characters removed. The initial encodeURIComponent step allows any UTF-16 string in JavaScript to then be safely encoded to base64url in a web browser. The final step of removing padding removes the “=” character which might be subject to further percent-encoding as part of a URL.

With Python, the JSON body above can be encoded into a content state URL like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import base64
from urllib import parse
import json

def encode_content_state(plain_content_state):
    uri_encoded = parse.quote(plain_content_state, safe='')  # equivalent of encodeURIComponent
    utf8_encoded = uri_encoded.encode("UTF-8")
    base64url = base64.urlsafe_b64encode(utf8_encoded)
    utf8_decoded = base64url.decode("UTF-8")
    base64url_no_padding = utf8_decoded.replace("=", "")
    return base64url_no_padding

if __name__ == "__main__":
    my_file = open('sample_range.json')
    x = json.load(my_file)
    encode_content_state(json.dumps(x))

This will return an encoded string that can decoded by a viewer that supports the specification. An anchor can be built that passes this string following content state convention:

1
2
3
<a href="https://link_to_viewer?iiif_content=JTdCJTIyJTQwY29udGV4dCUyMiUzQSUyMCUyMmh0dHAlM0ElMkYlMkZpaWlmLmlvJTJGYXBpJTJGcHJlc2VudGF0aW9uJTJGMyUyRmNvbnRleHQuanNvbiUyMiUyQyUyMCUyMmlkJTIyJTNBJTIwJTIyaHR0cHMlM0ElMkYlMkZkaWdpdGFsLmxpYi51dGsuZWR1JTJGYXNzZW1ibGUlMkZjb250ZW50LXN0YXRlcyUyRjElMjIlMkMlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyQW5ub3RhdGlvbiUyMiUyQyUyMCUyMm1vdGl2YXRpb24lMjIlM0ElMjAlNUIlMjJjb250ZW50U3RhdGUlMjIlNUQlMkMlMjAlMjJ0YXJnZXQlMjIlM0ElMjAlN0IlMjJpZCUyMiUzQSUyMCUyMmh0dHBzJTNBJTJGJTJGZGlnaXRhbC5saWIudXRrLmVkdSUyRm5vdGRlcmZlcmVuY2VhYmxlJTJGYXNzZW1ibGUlMkZtYW5pZmVzdCUyRnJmdGElMkY4JTJGcmFuZ2UlMkZwbGFjZXNfbWVudGlvbmVkJTJGMSUyMiUyQyUyMCUyMnR5cGUlMjIlM0ElMjAlMjJSYW5nZSUyMiUyQyUyMCUyMnBhcnRPZiUyMiUzQSUyMCU1QiU3QiUyMmlkJTIyJTNBJTIwJTIyaHR0cHMlM0ElMkYlMkZkaWdpdGFsLmxpYi51dGsuZWR1JTJGYXNzZW1ibGUlMkZtYW5pZmVzdCUyRnJmdGElMkY4JTIyJTJDJTIwJTIydHlwZSUyMiUzQSUyMCUyMk1hbmlmZXN0JTIyJTdEJTVEJTdEJTdE">
   Link to Viewer
</a>

Then, the consuming application can decode this URI according to the specification like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import base64
from urllib import parse
import json

def decode_content_state(encoded_content_state):
    padded_content_state = restore_padding(encoded_content_state)
    base64url_decoded = base64.urlsafe_b64decode(padded_content_state)
    utf8_decoded = base64url_decoded.decode("UTF-8")
    uri_decoded = parse.unquote(utf8_decoded)
    return uri_decoded


if __name__ == "__main__":
   content_state = "JTdCJTIyJTQwY29udGV4dCUyMiUzQSUyMCUyMmh0dHAlM0ElMkYlMkZpaWlmLmlvJTJGYXBpJTJGcHJlc2VudGF0aW9uJTJGMyUyRmNvbnRleHQuanNvbiUyMiUyQyUyMCUyMmlkJTIyJTNBJTIwJTIyaHR0cHMlM0ElMkYlMkZkaWdpdGFsLmxpYi51dGsuZWR1JTJGYXNzZW1ibGUlMkZjb250ZW50LXN0YXRlcyUyRjElMjIlMkMlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyQW5ub3RhdGlvbiUyMiUyQyUyMCUyMm1vdGl2YXRpb24lMjIlM0ElMjAlNUIlMjJjb250ZW50U3RhdGUlMjIlNUQlMkMlMjAlMjJ0YXJnZXQlMjIlM0ElMjAlN0IlMjJpZCUyMiUzQSUyMCUyMmh0dHBzJTNBJTJGJTJGZGlnaXRhbC5saWIudXRrLmVkdSUyRm5vdGRlcmZlcmVuY2VhYmxlJTJGYXNzZW1ibGUlMkZtYW5pZmVzdCUyRnJmdGElMkY4JTJGcmFuZ2UlMkZwbGFjZXNfbWVudGlvbmVkJTJGMSUyMiUyQyUyMCUyMnR5cGUlMjIlM0ElMjAlMjJSYW5nZSUyMiUyQyUyMCUyMnBhcnRPZiUyMiUzQSUyMCU1QiU3QiUyMmlkJTIyJTNBJTIwJTIyaHR0cHMlM0ElMkYlMkZkaWdpdGFsLmxpYi51dGsuZWR1JTJGYXNzZW1ibGUlMkZtYW5pZmVzdCUyRnJmdGElMkY4JTIyJTJDJTIwJTIydHlwZSUyMiUzQSUyMCUyMk1hbmlmZXN0JTIyJTdEJTVEJTdEJTdE"
   decode_content_state(content_state)

Assuming the viewer supports content state, the video can then start at the same temporal media fragment as in the Beck Jackson example above.

While this may all seem like a big ask, I personally want to IIIF and its various APIs to power our future repositories. In order to make this happen, consuming applications must be able to recognize, encode, and decode content state. While we aren't there yet, I am hopeful we will see this support in the future.