DRI v15.12

Release 15.12 of the repository software has been tagged and is running on the production system. This was primarily a maintenance release focused on refactoring of code and updating dependencies.

This release includes:

You can access the repository at https://repository.dri.ie.

DRI nearby?

Have you ever been out walking and wondered if DRI contains any objects related to your current location? No, neither have I, but having taken a short online course in Android development I thought I’d put together a simple app to do this.

First we need a basic Android application that will display a map. There are plenty of guides available on how to do this, for example here.

Next is to find out where the user is and to receive updates on their location as they move. How to do this is covered well in the Android developer guide.

A very brief summary is:

Give the application permission to access location services in the app manifest

<uses-permission 
    android:name="android.permission.ACCESS_FINE_LOCATION"/>

Create an API client and a LocationRequest object

mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API)
                .build();

// Create the LocationRequest object
mLocationRequest = LocationRequest.create()
               .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
               .setInterval(10 * 1000) // 10 seconds
               .setFastestInterval(1 * 1000); // 1 second

Once the client is connected (i.e., in the onConnected() callback) request location updates

LocationServices.FusedLocationApi.requestLocationUpdates(
            mGoogleApiClient, mLocationRequest, this);

The current location will be updated through the onLocationChanged() callback

@Override
public void onLocationChanged(Location location) {
     handleNewLocation(location);
}

Each time we receive a location update we process it in the handleNewLocation() method. In this method the user’s current latitude and longitude are retrieved, a marker is added to the map, and this location is then passed to the code that handles finding the nearby objects from the DRI repository.

private void handleNewLocation(Location location) {
        Log.d(TAG, location.toString());

        // Get the current latitude and longitude
        double currentLatitude = location.getLatitude();
        double currentLongitude = location.getLongitude();

        // Add a marker to the map showing the user where they are
        LatLng latLng = new LatLng(currentLatitude, currentLongitude);
        MarkerOptions options = new MarkerOptions()
                .position(latLng)
                .title("You are here!");
        mMap.addMarker(options);

        // Move and zoom the camera to their location
        mMap.animateCamera(CameraUpdateFactory
                           .newLatLngZoom(latLng, 12.0f));

        // Find and display the locations of nearby objects
        String locationUrl = String.format(url, 
                 currentLatitude, currentLongitude);
        new GetObjectLocations(locationUrl, 
                 currentLatitude, currentLongitude).execute();
}

In a previous post I talked about mapping in the DRI repository. Adding the mapping interfaces involved configuring Solr to support spatial searches. We can use this functionality here to retrieve the objects close to the user’s location.

The url to perform a spatial query looks like this:

https://repository.dri.ie/catalog?spatial_search_type=point
&coordinates=53.3483,-6.2572&format=json

The response is in JSON format and contains the Solr documents of all the objects matching the query (i.e., all the objects within a certain spatial distance of the given co-ordinates). Rather than display every object the app will only add markers at each place, with the number of objects at that place added as a label. Querying the repository and processing the results is all performed in an AsyncTask separate to the MainActivity. This is done as the network request and subsequent processing could take some time and the app should not stop responding during this.

To retrieve the response a connection is made to the search URL followed by a GET request

URL url = new URL(requestUrl);
// http client
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
is = conn.getInputStream();
// Convert the InputStream into a string
contentAsString = readIt(is);
// Reads an InputStream and converts it to a String.
    public String readIt(InputStream stream) 
      throws IOException {
        BufferedReader reader =new BufferedReader(
            new InputStreamReader(stream, "UTF-8"));
        String response = "",data="";

        while ((data = reader.readLine()) != null){
            response += data;
        }

        return response;
}

contentAsString contains the JSON response and this is returned for parsing.

The org.json API is used to parse the JSON. As stated above, as the app will only display counts of the objects at locations we can parse the facet output contained in the response rather than each object.

// Create a JSONObject from the String response
JSONObject jsonObj = new JSONObject(jsonStr);

// Get the facets for each place
JSONArray places = getPlacesFromFacets(jsonObj);

if (places != null) {
  // looping through facets
  for (int i = 0; i < places.length(); i++) {
    JSONObject p = places.getJSONObject(i);
    // How many objects at this place
    String hits = p.getString(TAG_HITS);
        
    // Get the GeoJSON formatted place metadata
    String geojson_ssim = p.getString(TAG_VALUE);
    JSONObject geojsonObject = new JSONObject(geojson_ssim);

    // Get the placename
    String placename = getPlacename(geojsonObject);

    JSONObject geometry = geojsonObject
      .getJSONObject(TAG_GEOMETRY);
    String type = geometry.getString(TAG_TYPE);

    // Only consider points, not bounding boxes
    if (type.equals("Point")) {
      // Get the co-ordinates of the place
      JSONArray coordinates = geometry
        .getJSONArray(TAG_COORDINATES);

      String latitude = coordinates.getString(1);
      String longitude = coordinates.getString(0);

      // As some objects contain multiple places, only
      // use those within the required range
      if (withinRange(latitude, longitude)) {
        HashMap<String, String> placeEntry = 
          new HashMap<>();
        placeEntry.put(TAG_HITS, hits);
        placeEntry.put(TAG_LATITUDE, latitude);
        placeEntry.put(TAG_LONGITUDE, longitude);

        placeList.put(placename, placeEntry);
      }
  }
}
}

For each entry added to the placeList a marker is added to the map.

private void addMarker(String placename, HashMap<String, String> placeEntry){
  String noOfObjects = placeEntry.get(TAG_HITS);
  String latitude = placeEntry.get(TAG_LATITUDE);
  String longitude = placeEntry.get(TAG_LONGITUDE);

  MarkerOptions options = new MarkerOptions()
             .icon(BitmapDescriptorFactory.fromBitmap(
                       iconFactory.makeIcon(noOfObjects)))
             .position(new LatLng(Double.valueOf(latitude),
                       Double.valueOf(longitude)))
             .anchor(iconFactory.getAnchorU(), 
                     iconFactory.getAnchorV())
             .title(placename);
  mMap.addMarker(options);
}

The marker shows the number of objects.

User's location and the place markers displayed on the map

Clicking on the marker displays the placename.

Place title links to Repository search

Clicking on the title opens the device’s browser and displays the objects in the repository that contain that placename in their metadata.

Nearby objects displayed in Repository

The screenshots above show the working application running on an emulator of a Nexus 5.

DRI DIY: Building a bookshelf

During the run up to the DRI launch, to take a break from ingesting collections, I was playing around with a JavaScript page-turner. I re-visited that recently to make it work through the repository API, rather than connecting directly to the Solr service. This was so that it could be used as a standalone application outside of the local network. I took this as an opportunity to play around with some JavaScript and CSS in Rails. I guess it turned into more of an exercise in self-assembly rather than DIY, as I pieced a lot of existing code together.

(tl;dr demo here: http://bit.ly/1LuoEkQ)

Turn.js is a JavaScript library that makes it easy to create a nice looking flip book. With this library the code needed to create and display a book with hardcoded pages was straightforward. The idea was to build the book dynamically using images from a collection stored in the DRI repository. To do this it was necessary to have a way to translate page numbers to object IDs. For at least one of our collections this was possible thanks to a field in the object metadata that could be mapped to the page number:

<dc:identifier>1915_0002</dc:identifier>

By using a Ruby template string, the identifier can be obtained from the page number quite easily:

page = 2
template = "1915_%04d"

identifier = template % page

The object’s ID can be retrieved by submitting a query to the repository containing this identifier. Once we have the ID the list of available images for that object can be retrieved with another API call. The repository returns a JSON list of file URLs. Using the URL from this list that corresponds to the format needed the page can be created and added to the book.

The page-turner

Another post, https://github.com/blasten/turn.js/wiki/Making-pages-dynamically-with-Ajax, gave the steps needed to dynamically add pages to the book. An AJAX call is made to the page turner app controller that handles all the steps above, and returns a DIV containing the page URL.

def page
    book_id = params[:book_id]
    page = params[:id]

    # Find the PageTurner model for this book
    turner = PageTurner.find(book_id)

    # Use the model to retrieve the image 
    # URL to use for the given page number
    url = turner.page_url(page) if turner

    # Return the HTML to add to the flip book
    render text: "<div><img src='#{url}'/></div>"
end

For no reason other than I thought it might look nice I started looking at how to add a bookshelf to hold the ‘books’. Finding and following this blog post, http://www.hmp.is.it/making-a-fancy-book-using-html5-canvases/, showed how it could be done. It makes use of CSS, HTML5 and JavaScript. Taking the code and integrating it with the Rails asset pipeline allowed the book to be displayed on a ‘shelf’. Clicking the book links through to the flip book.

The bookshelf

The final stage was to try out Heroku to deploy the app as a demo. You should be able to access it at https://dry-savannah-3204.herokuapp.com/books. I’m using the free service, so it might not always be available.

Obviously this isn’t the cleanest solution, and it could do with a lot of tweaking, but as an exercise in the Rails pipeline it was quite useful.

DRI v15.08

A new release of the repository software, the first since launch, has been deployed to the production system. This release includes:

  • upgraded Hydra, ActiveFedora, sufia-models versions
  • changed workflow for managing organisations
  • editor’s can remove draft objects and individual assets attached to draft objects
  • completed DOI implementation

Completing the DOI functionality means we can now begin the process of minting DOIs for the collections currently published in the repository. Initially this will be done on a collection by collection basis. Once configured, collections ingested in the future will be assigned DOIs at time of publication.

You can access the repository at https://repository.dri.ie.

Mapping with added SPARQL

One of the most requested features for the Digital Repository of Ireland was a mapping interface for the objects stored in the repository. As the repository is built using Hydra, which includes the Blacklight Solr discovery interface, we were able to quickly add this using Blacklight Maps.

Objects displayed on map

Blacklight Maps adds a set of mapping views to the Blacklight UI. An extra field, geojson_ssim, added to the Solr index specifies the objects location on the map. This field is formatted in GeoJSON, which looks like the following (taken from the Blacklight Maps docs):

  geojson_ssim:
   - {"type":"Feature","geometry":{"type":"Point","coordinates":[78.96288,20.593684]},"properties":{"placename":"India"}}

For DRI an object’s location is contained in its descriptive metadata in XML format. In Dublin Core the dcterms:spatial term is used:

<dcterms:spatial xsi:type="dcterms:Point">
name=Talbot Street, Dublin; north=53.350346; east=-6.256991
</dcterms:spatial>

On ingestion this term is transformed to the required GeoJSON format before it is stored in the Solr index.

An extra feature that we support is the use of Linked Data URIs within an object’s spatial metadata, specifically Linked Logainm URIs. The benefit of supporting URIs is that it allows the cataloguer to enrich their metadata with spatial information using the Logainm RDF reconciliation service through tools such as OpenRefine. The Logainm technical document gives more information on how this is achieved.

Turning the URIs contained in the XML, like this:

<dcterms:spatial>http://data.logainm.ie/place/18255</dcterms:spatial>

into the required GeoJSON format Solr field is a two stage process. The first step is where SPARQL is used. A SPARQL query, with the uri value from the XML metadata, retrieves the co-ordinates from the Linked Logainm SPARQL endpoint:

select = 
  "select ?nameEN, ?nameGA, ?lat, ?long
   where { <#{uri}> <http://xmlns.com/foaf/0.1/name> ?nameGA, ?nameEN;
   <http://geovocab.org/geometry#geometry> ?g . ?g geo:lat ?lat; 
   geo:long ?long .
   filter(lang(?nameEN)=\"en\" && lang(?nameGA)=\"ga\") . }"

client = DRI::Sparql::Client.new @endpoint
results = client.query select

For a full explanation of the structure of the query see the Linked Logainm technical document.

The client makes use of the Ruby Sparql client to send the query to the Logainm endpoint and to retrieve the results.

def initialize(endpoint)
   @sparql = SPARQL::Client.new(endpoint)
end
begin
  results = @sparql.query(select)
rescue Exception => e
  Rails.logger.error "Unable to query sparql endpoint: #{e.message}"
end

The second step is to iterate through the result set to create the co-ordinates for the location and to then transform these to the GeoJSON Solr field for display on the map:

results.each_solution do |s|
  name = "#{s[:nameGA].value}/#{s[:nameEN]}"
  north = s[:lat].value
  east = s[:long].value
  points << DRI::Metadata::Transformations.geojson_string_from_coords(
                                            name, "#{east} #{north}")
end