Geotagging at SFO Museum, Part 6 – Writers

This is a blog post by aaron cope that was published on April 30, 2020 . It was tagged sfo, collection, geotagging and golang.

children’s souvenir logbook: BOAC (British Overseas Airways Corporation). Paper, ink. Gift of Dr. John W. Taylor, SFO Museum Collection. 2011.211.003 a b.

This is the sixth of an 11-part blog post about geotagging photos in the SFO Museum collection. At the end of the last post I wrote:

Have you noticed the Save button in some of the screenshots above? We’ll talk about that in the next blog post.

So far, we’ve got a map and a camera, a global search endpoint, a simple way to query for and display images and enough information to display already geotagged images correctly.

Importantly none of these things are specific to our museum or any other institution. In fact there’s nothing about go-www-geotag that is specific to the cultural heritage sector at all. You could use this tool to geotag any set of images.

Installation view of The Typewriter: An Innovation in Writing. Photograph by SFO Museum.

But what about saving the geotagging information that the Leaflet.GeotagPhoto extension produces? By default, the go-www-geotag application prints the raw geotagging data in to the GeoJSON pane below the map. Failing all else you could copy-and-paste that location information from the application to wherever it needs to go but that’s hardly an ideal scenario.

In the last post I said that:

Rather than trying to support a potentially infinite list of image sources (to read from) we’ve decided to require the use of the oEmbed standard as the means by which images are identified and loaded in to the application.

Unfortunately there isn’t really a similar standard for writing data. Writing data gets in to all kinds of complicated issues like authentication and authorization, validations and transformations, and more generally workflows and editorial processes. These are all things that are almost never implemented the same way from one institution or organization to the next, nor should they be. The go-www-geotag application attempts to address this problem by introducing the concept of abstract “writers” for data. In other words it tries to make all the details someone else’s problem.

Writers are implemented as an “interface” in the Go programming language. Interfaces are sometimes also called “protocols” or “contracts” in other languages but the principle is the same: They define functionality independent of implementation that other pieces of code adhere to.

type Writer interface {
	WriteFeature(context.Context, string, *geotag.GeotagFeature) error
}

The go-www-geotag.Writer interface defines a single method, called WriteFeature that takes three input parameters:

  • A Go language context.Context value (which you don’t need to worry about right now).
  • A unique identifier which we’ll talk about in the next blog post.
  • A geotag.GeotagFeature value. This wraps the raw GeoJSON data produced by the Leaflet.GeotagPhoto plugin and provides additional functionality on top of that data. It also means that different writers have a consistent way to accept and work with geotagging data.

At the moment the go-www-geotag application only has one built-in writer, called StdoutWriter, which prints its output to the terminal that you’ve started the application from.

Here’s an abbreviated example of the code for the StdoutWriter:

type StdoutWriter struct {
	Writer	// this declares that "StdoutWriter" will implement the methods required by "Writer"
}

func (wr *StdoutWriter) WriteFeature(ctx context.Context, uri string, f *geotag.GeotagFeature) error {

	body, _ := json.Marshal(f)
	br := bytes.NewReader(body)
	
	io.Copy(os.Stdout, br)	
	return nil
}

A writer that simply parrots things to a terminal window has limited utility. In time we plan to include more sophisticated writers with the go-www-geotag application that might output geotagging data to a file on disk or even a database connection.

The important thing to understand is that writers are implemented in code and, in many cases, code that might be specific to your application. This is where go-www-geotag stops being a generic application designed for multiple uses and becomes a bespoke tool for a dedicated purpose. We’ll demonstrate an example of that in subsequent posts.

guest book: Association of Flight Attendants, Edith Lauterbach. Paper, ink, plastic, metal. Gift of Edith Lauterbach, SFO Museum Collection. 2006.028.148 a e.

The reason for requiring this level of indirection (complexity, even) is that it allows the go-www-geotag application to be unconcerned with how data is output or where it is written to. The application expects to be given a valid writer.Writer instance and after parsing and validating input data assumes the writer will be responsible for everything else.

Here’s an abbreviated example of what that looks like in code:

import (
       "github.com/sfomuseum/go-geojson-geotag"
       "github.com/sfomuseum/go-www-geotag/writer"
       "net/http"
)

func WriterHandler(wr writer.Writer) (http.Handler, error) {

	// If you're wondering: There is built-in support for "crumbs"
	// to prevent cross-site request forgeries (CSRF) in the
	// WriterHandler but we've omitted it here for the sake of brevity
		
	fn := func(rsp http.ResponseWriter, req *http.Request) {

		ctx := req.Context()
		query := req.URL.Query()

		uid := query.Get("id")		      		
		geotag_f, _ := geotag.NewGeotagFeatureWithReader(req.Body)
		wr.WriteFeature(ctx, id, geotag_f)

		return
	}

	h := http.HandlerFunc(fn)
	return h, nil
}

wr, _ := writer.NewWriter("stdout://")
wr_handler, _ := WriterHandler(wr)

mux := http.NewServeMux()
mux.Handle("/update, wr_handler)

To enable support for writers you’d start the application like this:

./bin/server \
	-nextzen-apikey {NEXTZEN_API_KEY} \
	-enable-placeholder \
	-placeholder-endpoint {PLACEHOLDER_API_ENDPOINT} \
	-enable-oembed \
	-oembed-endpoints 'https://millsfield.sfomuseum.org/oembed/?url={url}&format=json'
	-enable-writer \
	-writer-uri stdout://

2020/04/15 08:58:22 Listening on http://localhost:8080

Once enabled the application will display a Save button over the map as soon as you’ve positioned or updated the camera.

When you press the Save button the application will call its internal “update” endpoint and you’ll see something like this on the terminal console:

...
2020/04/15 16:55:12 Listening on http://localhost:8080
{"id":"151/194/350/1/1511943501.geojson","type":"Feature","geometry":{"type":"GeometryCollection","geometries":[{"coordinates":[-122.2277069091797,37.72952984064848],"type":"Point"},{"coordinates":[[-122.20044810528073,37.72790237562358],[-122.20449700045866,37.71810922464792]],"type":"LineString"}]},"properties":{"angle":27.56984642624502,"bearing":108.09350033180942,"distance":2335.609119659562}}

Which is the same, unformatted, data you can see in the application’s GeoJSON pane. In the next post we’ll demonstrate how to implement a custom writer that extends these ideas in to something a little more useful.