Geotagging at SFO Museum, Part 6 – Writers
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.
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.
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.