Overview

In MarkLogic 11, you can now store and query geospatial data using the Optic engine, otherwise known as Optic Geo. Querying your location data just became even more practical through the combined forces of Template Driven Extraction (TDE), the Optic engine, and the Geospatial Region Index. Now you can:

  • Perform searches on geospatial data using multiple interfaces, including SQL, SPARQL, and the Optic API.
  • Evaluate DE-9IM relationships against your stored regions.
  • Export, interact with, and filter through your stored regions in your favorite OpenGIS-supported tool via ODBC.

Some example use cases include:

  • Find all stored points within radius R of center point P.
  • Find all stored polygons contained in polygon P.
  • Find all stored regions that overlap polygon P.

Background

Querying geospatial data with Optic builds upon the pre-existing geospatial region index. We bridged the gap between the geospatial region index and the triple index with the release of MarkLogic 11. Optic Geo enhances the capabilities of the triple index by providing the means to index and query geospatial regions to discover DE-9IM spatial relations. You can store a variety of geometry types, including points, boxes, circles, linestrings, polygons, and complex polygons (see supported geometry types).

Let’s run through a few examples. These steps require a licensed version of MarkLogic 11 to be installed. Feel free to copy-paste the queries into QConsole and run them against the Documents database to follow along. It may be best to create a new database for this tutorial and run the queries against it to avoid unexpected results. All of the below queries are in JavaScript, so be sure to select this query type in Query Console.

Before getting started, it’s important to know the distinction between MarkLogic’s internal serialization of points and well-known text (WKT) points. In MarkLogic’s point geometry constructor cts.point() and its output serialization, the first argument is the latitude and the second argument is the longitude. However, with WKT , a point’s longitude is written first, then the latitude.

For example, if this JavaScript is run:

let point = cts.point(37.52097045580761,-122.25619923947212)
point;

MarkLogic will return

37.52097,-122.2562

If this JavaScript is run, which converts the point to a WKT string:

let point = geo.toWkt(cts.point(37.52097045580761,-122.25619923947212))
point;

MarkLogic will return

POINT(-122.2562 37.52097)

A region is in WKT if the coordinates are prefixed by the Geometry type.

If there is a geometry type prefixing the coordinates AND the longitude and latitude are separated by a space, then it is WKT and longitude is shown first, followed by latitude.

However, if there is no geometry type prefix AND the latitude and longitude are separated by a COMMA, then it is NOT WKT and latitude is shown first, followed by longitude.

Like WKT, GeoJSON data also serializes points as [ longitude ,  latitude ].

Notice that Keyhole Markup Language (KML) is also written as “longitude, latitude”, and looks very similar to MarkLogic internal serialization. In fact, MarkLogic can parse KML out of the box BUT the latitude and longitude are switched. If inserting KML documents, be sure to transform the document by swapping the latitude and longitude.

Read the Search Developer Guide for more information on the cts region constructors.

Example Document: Single City

Document Insert: McMullen, Alabama

'use strict';
declareUpdate();
 
let node =
xdmp.unquote(
`{
  "Placemark": {
    "name": "McMullen",
    "geoid": 145640,
    "namelsad": "McMullen town",
    "state": "AL",
    "country": "US",
    "aland": 283764,
    "awater": 0,
    "intptlat": 33.1480998,
    "intptlon": -88.1757492,
    "hwyIntersection": "LINESTRING(-88.17394429612851 33.15023965571076,-88.1726997511456 33.14923359808806,-88.17184144426083 33.14851497843653,-88.17111188340878 33.14790414710443)",
    "region": "POLYGON((-88.17948 33.150064,-88.176493 33.150061,-88.174872 33.15006,-88.173956 33.150068,-88.172991 33.149269,-88.172777 33.149096,-88.171263 33.147854,-88.171291 33.146395,-88.177247 33.146363,-88.179577 33.14642,-88.17948 33.150064))"
  }
}`)
 
xdmp.documentInsert('McMullen.json', node)

The above document (McMullen.json) is sample data representing the town of McMullen, Alabama. Notice it has a latitude, longitude, a linestring WKT literal representing Route 14, and a polygon WKT literal. MarkLogic can recognize and extract WKT literals from your documents, as long as they are valid. You can also construct geometries from different elements or attributes in a document, as demonstrated below.

In this tutorial, we will use template driven extraction (TDE) to create a view on top of McMullen.json.

TDE View

Note that the example below assumes you have the requiredtde-admin role to insert a TDE template.

TDE Template for a View called “Towns”

'use strict';
declareUpdate();
var tde = require("/MarkLogic/tde.xqy");
 
let node =
{
  "template": {
    "description": "region table",
    "context": "/Placemark",
    "rows": [
      {
        "schemaName": "Regions",
        "viewName": "Towns",
        "columns": [
          {
            "name": "geoid",
            "scalarType": "int",
            "val": "geoid",
            "nullable" : false
          },
          {
            "name": "name",
            "scalarType": "string",
            "val": "name"
          },
          {
            "name": "interiorPoint",
            "scalarType": "point",
            "val": "cts:point(intptlat,intptlon)",
            "coordinateSystem": "wgs84"
          },
          {
            "name": "highway",
            "scalarType": "linestring",
            "val": "cts:linestring(hwyIntersection)",
            "nullable" : true,
            "invalidValues": "ignore",
            "coordinateSystem": "wgs84"
          },
          {
            "name": "tenKmRadius",
            "scalarType": "circle",
            "val": "cts:circle(10,cts:point(intptlat,intptlon))",
            "invalidValues": "reject",
            "coordinateSystem": "wgs84",
            "units": "km"
          },
          {
            "name": "exactGeometry",
            "scalarType": "polygon",
            "val": "cts:polygon(region)",
            "invalidValues": "ignore",
            "coordinateSystem": "wgs84"
          }
        ]
      }
    ]
  }
}
 
tde.templateInsert('towns.tdej', node)

The above excerpt is an example of how to extract different geometries from documents. Notice how the geometry regions are constructed using the cts:* geometry constructors. You can pass a WKT string to all of thects:region()constructors, except for cts:circle() and cts:box(), as there is no WKT representation for those shapes. You may find it useful to convert your boxes and circles to polygons to represent them as WKT. Converting circles to polygons is also particularly useful for optimizing queries that involve finding points within a certain radius of another point. See Advanced Optic Geo Topics for these examples.

Each valid geometry in your document that matches a TDE val will be indexed, and optimized queries will be enabled against it.

TheinteriorPointcolumn is constructed with the constructor cts:point() , extracting the values at intptlat and intptlon from the JSON.

Thehighwaylinestring column creates linestrings using thects:linestring() constructor, and a WKT linestring is extracted from the sample document.

The objecttenKmRadius is a circle. The units element in the TDE above delegates the units of the circle to be in km (kilometers). This means that the first argument (radius) passed to cts:circle() in the val of tenKmRadius is in kilometers. Other valid values forunits are miles, feet, and meters, with miles being the default. After this circle is created and indexed, it represents a 10-kilometer radius around McMullen, Alabama.

Finally, our exactGeometry column is a cts:polygon constructed by a WKT string literal, representing the exact bounds of our example town.

TDEs also accept a generic region for the scalarType. See the Advanced Optic Geo section for more information.

Notice that all of our geometries are in the wgs84 coordinate system, as delegated by the TDE template. Internally, the points are stored as floats in the wgs84 coordinate system. If our application requires higher precision and more exact points, e.g. to be able to tell which side of the street a point is on, use the wgs84-double coordinate system. For more information about coordinate systems, see Advanced Optic Geo Topics and the Geospatial Chapter of the Search Developer Guide. Additionally, see Advanced Optic Geo Topics to learn how MarkLogic determines the governing coordinate system for insertion and query.

Now that we’ve created our view and indexed some geometries, let’s run some queries!

SQL Examples

First, let’s see what our view looks like so far.

SQL Select*

'use strict';
xdmp.sql("select * from towns")

The query above will return all the data extracted from McMullen.json. The full row has a column for each: geoid, name, interior point, highway, 10-kilometer radius, and exact geometry.

Next, let’s run a query to see if our data is covered by a WKT literal representing Alabama.

SQL ST_CoveredBy
'use strict';
 
xdmp.sql("select * from towns where ST_CoveredBy(exactGeometry, 'POLYGON((-88.10858972561232 35.01358588444459,-88.46015222561232 30.92273533487532,-88.37226160061232 30.355585086994857,-87.38349206936232 30.374543579358562,-87.55927331936232 30.99810354829685,-85.05439050686232 31.03576534077649,-84.90058191311232 32.43724515652905,-85.60370691311232 35.01358588444459,-88.10858972561232 35.01358588444459))')")

This will return the row representing McMullen because it is covered by our crude representation of Alabama!

Next, let’s do the inverse of this to be sure that Alabama is not covered by McMullen.

SQL ST_Covers
'use strict';
 
xdmp.sql("select * from towns where ST_Covers(exactGeometry, 'POLYGON((-88.10858972561232 35.01358588444459,-88.46015222561232 30.92273533487532,-88.37226160061232 30.355585086994857,-87.38349206936232 30.374543579358562,-87.55927331936232 30.99810354829685,-85.05439050686232 31.03576534077649,-84.90058191311232 32.43724515652905,-85.60370691311232 35.01358588444459,-88.10858972561232 35.01358588444459))')")

And indeed, we get no rows back, as McMullen does not satisfy the DE-9IM relationship COVERS with our rough Alabama WKT polygon.

For our final SQL example, let’s see if Aliceville, a neighboring town in Alabama, is within 10 kilometers of McMullen.

SQL ST_Covers Radius
'use strict';
xdmp.sql("select * from towns where ST_Covers(tenKmRadius, 'POINT(-88.15173300182254 33.129683056012254)')")

Yep. We get our row back. POINT(-88.15173300182254 33.129683056012254), a point representing Aliceville, is covered by our circle representing a 10-kilometer radius around McMullen.

To see all possible DE-9IM predicates, see the Related Resources section below

Example Document: Multiple Cities

Only getting one row back is not that interesting, so first, let’s insert some more data!

In real-world use cases, you’re likely to have much more complex data in individual documents or archives and be loading with MLCP, but to keep it simple, let’s continue using xdmp.documentInsert(). 

Document Insert: Multiple Cities
'use strict';
declareUpdate();
 
let node =
xdmp.unquote(
`{
  "Placemark": {
    "name": "Panorama Heights",
    "geoid": 655506,
    "namelsad": "Panorama Heights CDP",
    "state": "CA",
    "country": "US",
    "aland": 1230482,
    "awater": 0,
    "intptlat": 35.806767,
    "intptlon": -118.62705,
    "region": "POLYGON((-118.63555 35.809731,-118.62843 35.809727,-118.62845 35.810692,-118.6284 35.810898,-118.61921 35.811218,-118.61917 35.808052,-118.61921 35.803162,-118.62955 35.802883,-118.6355 35.803001,-118.63566 35.80822,-118.63555 35.809731))"
  }
}`)
 
xdmp.documentInsert('PanoramaHeights.json', node);
 
let node1 =
xdmp.unquote(
`{
  "Placemark": {
    "name": "Aliceville",
    "geoid": 101228,
    "namelsad": "Aliceville city",
    "state": "AL",
    "country": "US",
    "aland": 11819855,
    "awater": 0,
    "intptlat": 33.1236861,
    "intptlon": -88.1593640,
    "region": "POLYGON((-88.17092776174728 33.1404251055091,-88.17247271413986 33.121018361271275,-88.18139910574142 33.11814292321007,-88.18225741262619 33.095855086608005,-88.16251635427658 33.098299835016434,-88.16268801565353 33.11900556451336,-88.13625216360275 33.11914933724016,-88.13487887258712 33.140856317762605,-88.17092776174728 33.1404251055091))"
  }
}`)
 
xdmp.documentInsert('Aliceville.json', node1);
 
let node2 =
xdmp.unquote(
`{
  "Placemark": {
    "name": "Petrey",
    "geoid": 159328,
    "namelsad": "Petrey town",
    "state": "AL",
    "country": "US",
    "aland": 1900154,
    "awater": 42953,
    "intptlat": 31.8521599,
    "intptlon": -86.2040072,
    "region": "POLYGON((-86.21253 31.856297,-86.209977 31.856288,-86.207793 31.856282,-86.202915 31.856265,-86.198887 31.856253,-86.198907 31.851911,-86.198908 31.851545,-86.198909 31.850345,-86.19889 31.848558,-86.198804 31.8427,-86.20636 31.842627,-86.21239 31.842679,-86.212452 31.847102,-86.21252 31.852723,-86.21253 31.856297))"
  }
}`)
 
xdmp.documentInsert('Petrey.json', node2);
 
let node3 =
xdmp.unquote(
`{
  "Placemark": {
    "name": "Alta Sierra",
    "geoid": 601360,
    "namelsad": "Alta Sierra CDP",
    "state": "CA",
    "country": "US",
    "aland": 21551197,
    "awater": 53117,
    "intptlat": 39.1261741,
    "intptlon": -121.0490846,
    "hwyIntersection": "LINESTRING(-121.07102279357909 39.14123824552831,-121.07205276184081 39.13378205333755,-121.07514266662596 39.12938785605334,-121.07763175659179 39.12659140593477,-121.0783184020996 39.1223299351128,-121.08037833862304 39.117002734051624,-121.08218078308104 39.113406645626156)",
    "region": "POLYGON((-121.06520408014406 39.144847544675905,-121.08065360406984 39.11581869580818,-121.07241385797609 39.117683293332234,-121.0782503447925 39.104363657096705,-121.06314414362062 39.090242094334336,-121.0569643340503 39.09610421844091,-121.05353110651124 39.0905085650926,-121.04769461969484 39.096637114650186,-121.017825540105 39.13472875412051,-121.03670829156984 39.15150385055818,-121.05730765680421 39.148042650063246,-121.06520408014406 39.144847544675905))"
  }
}`)
 
xdmp.documentInsert('AltaSierra.json', node3);
 
let node4 =
xdmp.unquote(
`{
  "Placemark": {
    "name": "Las Flores",
    "geoid": 640536,
    "namelsad": "Las Flores CDP",
    "state": "CA",
    "country": "US",
    "aland": 926091,
    "awater": 0,
    "intptlat": 40.0721410,
    "intptlon": -122.1573122,
    "region": "POLYGON((-122.16636 40.076085,-122.16625 40.075915,-122.16621 40.075867,-122.16491 40.075834,-122.15803 40.075686,-122.15691 40.075657,-122.15476 40.07563,-122.15198 40.075595,-122.1517 40.074724,-122.15101 40.073004,-122.14898 40.068407,-122.14897 40.068356,-122.14958 40.068355,-122.1513 40.068328,-122.1527 40.06834,-122.15382 40.068356,-122.15988 40.068461,-122.16113 40.068467,-122.16131 40.068264,-122.16146 40.068491,-122.16657 40.075975,-122.16636 40.076085))"
  }
}`)
 
xdmp.documentInsert('LasFlores.json', node4);

Optic Examples

Let’s see our four new rows using an Optic query.

Optic select *
'use strict';
const op = require('/MarkLogic/optic');
 
const result=op.fromView('regions', 'towns')
               .orderBy('geoid')
               .result()
result;

We’ve successfully indexed data representing Las Flores (CA), Alta Sierra (CA), Petrey (AL), Aliceville (AL), and Panorama Heights(CA)!

How about we see which Alabamian towns DO NOT have a highway running through them?

Optic op.geo.coveredBy()
'use strict';
const op = require('/MarkLogic/optic');
 
const alabama=
`POLYGON((-88.10858972561232 35.01358588444459,
-88.46015222561232 30.92273533487532,
-88.37226160061232 30.355585086994857,
-87.38349206936232 30.374543579358562,
-87.55927331936232 30.99810354829685,
-85.05439050686232 31.03576534077649,
-84.90058191311232 32.43724515652905,
-85.60370691311232 35.01358588444459,
-88.10858972561232 35.01358588444459))`
 
const result=op.fromView('regions', 'towns')
               .where(op.geo.coveredBy(op.col('exactGeometry'),cts.polygon(alabama)))
               .where(op.not(op.isDefined(op.col('highway'))))
               .orderBy('geoid')
               .result()
result;

We can observe that Petrey and Aliceville are covered by our Alabama representation and have a value of null  for their highway column. We use the op.geo.coveredBy() value processing function in this case, as we are applying the constraint to each row in the towns view.

Next, let’s see how far our stored points are from Sacramento, California.

Optic op.geo.distance()
'use strict';
const op = require('/MarkLogic/optic');
 
const result=op.fromView('regions', 'towns')
               .bind(op.as('SacramentoDistance', op.geo.distance(cts.point('POINT(-121.49580152307632 38.577076831458754)'),op.col('interiorPoint'),'units=km')))
               .orderBy('SacramentoDistance')
               .select([op.col('name'),op.col('SacramentoDistance')])
               .result()
result;

Here, we get rows back representing names and distances of the column interiorPoint from Sacramento (represented as ‘POINT(-121.49580152307632 38.577076831458754)’) in kilometers. Looks like Alta Sierra, CA is the closest at about 72 km.

We use op.bind() here to bind the result of op.geo.distance() to a new column called SacramentoDistance. Various other useful geo functions can be found in the geo API docs.

Finally, let’s figure out which of our geometries are DISJOINT from Nevada County and within 300 miles of Memorial Hospital in Bakersfield, California.

Optic op.geo.disjoint()

 

'use strict';
const op = require('/MarkLogic/optic');
 
const nevadaCounty=
`POLYGON((-120.92944174178074 39.16872283252237,
-121.00634603865574 39.08775924801223,
-121.01733236678074 39.04297629387391,
-121.11620931990574 39.10481194713649,
-121.18762045271824 39.06643757507428,
-121.18762045271824 39.253847080683926,
-121.13268881209324 39.34948847864463,
-120.99810629256199 39.419542426623636,
-120.79760580428074 39.43439331405362,
-120.66851644881199 39.440757011220825,
-120.59710531599949 39.52555074455902,
-120.51745443709324 39.45348266144616,
-120.00659017928074 39.45136188123978,
-120.00933676131199 39.28361618540127,
-120.69323568709324 39.29637050178728,
-120.76464681990574 39.26022723908246,
-120.92944174178074 39.16872283252237))`
 
const result=op.fromView('regions', 'towns')
               .where(op.geo.disjoint(op.col('exactGeometry'),cts.polygon(nevadaCounty)))
               .where(op.lt(op.geo.distance(op.col('interiorPoint'),cts.point('POINT(-119.00685403770757 35.3915963125105)'),'units=miles'),300))
               .orderBy('geoid')
               .result()
result;

We see that Panorama Heights is DISJOINT from our WKT representation of Nevada County and less than 300 miles from Memorial Hospital. Alta Sierra also happens to be less than 300 miles from Memorial Hospital, however, it intersects Nevada County, and therefore we don’t see it in our results.

It’s important to note that op.geo.disjoint() is the only DE-9IM function that cannot be optimized because of limitations present in the geospatial region index. The same applies to ST_Disjoint() in SQL and geof:sfDisjoint() in SPARQL. Wherever possible, and especially with larger datasets, try to use other DE-9IM predicates to keep queries performant. See Advanced Optic Geo Topics for more information.

For more information on the Optic API, check out Getting Started with Optic and the Optic API Chapter of the Application Developer Guide. Now let’s move on to triples.

Inserting Managed Triples

In MarkLogic 11, we have implemented a subset of the OGC GeoSPARQL specification. This allows for geometry triples and enables DE-9IM relations to be resolved against them.

First, let’s insert a geo triple.

RDF Insert

declareUpdate();
const sem = require("/MarkLogic/semantics.xqy");
 
const triple = sem.triple(
  {
    "triple" : {
      "subject" : "http://example.org/ApplicationSchema#SugarloafVillage",
      "predicate" : "http://example.org/ApplicationSchema#hasExactGeometry",
      "object" : {
        "value" : "POLYGON((-118.63779 35.829243,-118.63585 35.829242,-118.6356 35.828962,-118.63539 35.828729,-118.63494 35.828367,-118.63462 35.828122,-118.63445 35.827563,-118.63409 35.827341,-118.63341 35.827339,-118.63313 35.827396,-118.633 35.82771,-118.6327 35.827848,-118.63248 35.827782,-118.6325 35.826541,-118.63244 35.82516,-118.63621 35.825073,-118.63766 35.825015,-118.63779 35.829243))",
        "datatype" : "http://www.opengis.net/ont/geosparql#wktLiteral"
      }
    }
  }
)
 
sem.rdfInsert(triple, null, null, "geograph")

In the excerpt above, we insert a sem:triple() with an object whose datatype is the IRI http://www.opengis.net/ont/geosparql#wktLiteral. This IRI tells MarkLogic to expect a WKT literal in the value of the object. The value of the object, (in this case a WKT representation of Sugarloaf Village, California) is then put into the geospatial region index and triple index, allowing SPARQL queries to be issued against it.

For more information on triples and semantic data, read the Semantics Guide

Let’s discover the triple we just inserted.

Triple Discovery

'use strict';
const sem = require("/MarkLogic/semantics.xqy");
 
let query =
`
PREFIX my: <http://example.org/ApplicationSchema#>
PREFIX geoml: <http://marklogic.com/geospatial#>
PREFIX cts: <http://marklogic.com/cts#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
 
SELECT *
WHERE { ?s my:hasExactGeometry ?o }
`
 
sem.sparql(query);

We see that the triple we just inserted has the subject my:SugarloafVillage and its object is the WKT literal representing the town itself.

You may observe that there is nowhere to specify a coordinate system in the objectof asem:triple(). What if you have a geometry that does not belong to your app server’s default coordinate system? See Advanced Optic Geo Topics for methods to specify a coordinate system for the geometry in the value, as well as for guidance on how MarkLogic determines the governing coordinate system for insertion and query.

You can also use TDE to extract geometry triples from documents.

TDE Triples

For the template we are about to insert, it’s possible to utilize the data we already have in the database. If you haven’t yet, run the scripts that added the documents above from both Example Document sections.

TDE Triple Region Extraction

'use strict';
declareUpdate();
var tde = require("/MarkLogic/tde.xqy");
 
let node =
{
  "template": {
    "description": "triple geom extraction",
    "context": "/Placemark",
    "triples": [
      {
        "subject": {
          "val": "sem:iri(fn:concat('http://example.org/ApplicationSchema#',fn:replace(name,' ','')))"
        },
        "predicate": {
          "val": "sem:iri('http://example.org/ApplicationSchema#hasExactGeometry')"
        },
        "object": {
          "val": "cts:polygon(region)"
        }
      }
    ]
  }
}
 
tde.templateInsert('townsTriples.tdej', node)

After executing the above, run the query labeled Triple Discovery again. We get triples that were extracted from our sample documents by the TDE template we have inserted! Familiar names of towns should appear in the subject along with their WKT representations.

You may notice that TDE triples have no means to specify a datatype like the object of sem:triple()s do. This implies that you must use the cts:* constructors to create a geometry in the subject, predicate, or object of a TDE triple. More information on the cts geometry constructors can be found in the Geospatial Chapter of the Search Developer Guide. Likesem:triple()s, there is no means to specify a coordinate system in TDE triple’s subject, predicate, or object. See Advanced Optic Geo Topics for methods to specify a coordinate system for the geometry in the val.

Now that we have some geo triples, let’s see what information we can discover using SPARQL.

SPARQL Examples

The GeoSPARQL functions in the OGC specification belong to the XML namespace, whose prefix is geof. Read the GeoSPARQL specification for more information.

In this example, let’s find out which of our exact geometries are in California.

geof:sfCoveredBy California

'use strict';
 
const sem = require("/MarkLogic/semantics.xqy");
 
const wktLiterals =
{
  california:
  `POLYGON((-124.37604812500001 42.095040851410936,
            -119.89362625000001 41.96447110189439,
            -119.98151687500001 38.921893259497516,
            -114.18073562500001 34.48491364418469,
            -114.48835281250001 32.72823394217145,
            -117.16901687500001 32.543198750921846,
            -118.57526687500001 33.90333689770918,
            -120.64069656250001 34.44868274474156,
            -122.57429031250001 37.680426957262384,
            -124.50788406250001 40.37676208553975,
            -124.37604812500001 42.095040851410936))`
}
 
let query =
`
PREFIX my: <http://example.org/ApplicationSchema#>
PREFIX geoml: <http://marklogic.com/geospatial#>
PREFIX cts: <http://marklogic.com/cts#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
 
SELECT *
WHERE { ?s my:hasExactGeometry ?o
FILTER geof:sfCoveredBy(?o,?california)
}
`
 
sem.sparql(query,wktLiterals);

We get our four triple geometries back whose geometry object is in California!

The JavaScript constant wktLiterals is passed in as bindings, allowing its attribute california  (a crude WKT polygon representation of the state) to be used in the SPARQL query.

Our SPARQL FILTER statement includes a call to the function geof:sfCoveredBy(). See the Related Resources section for more functions callable from SPARQL.

The geof:sf*  DE-9IM relation functions take in a third argument, a string array of options. If you are querying geo triples in a coordinate system other than the app server coordinate system, it is imperative to specify the coordinate system in this string array of options. See Advanced Optic Geo Topics for methods to pass a coordinate system to these functions.

As of the release of MarkLogic 11.0.0, SPARQL queries with a spatial constraint against large datasets with many triple geometries run slower than they should. Currently, the engine verifies the DE-9IM relation twice to avoid false positives from being returned; this will be improved in a later release. See Advanced Optic Geo Topics for more information.

Lastly, let’s chain some function calls to form a more interesting query.

SPARQL function chaining

'use strict';
 
const sem = require("/MarkLogic/semantics.xqy");
 
const wktLiterals =
{
  california:
  `POLYGON((-124.37604812500001 42.095040851410936,
            -119.89362625000001 41.96447110189439,
            -119.98151687500001 38.921893259497516,
            -114.18073562500001 34.48491364418469,
            -114.48835281250001 32.72823394217145,
            -117.16901687500001 32.543198750921846,
            -118.57526687500001 33.90333689770918,
            -120.64069656250001 34.44868274474156,
            -122.57429031250001 37.680426957262384,
            -124.50788406250001 40.37676208553975,
            -124.37604812500001 42.095040851410936))`
}
 
let query =
`
PREFIX my: <http://example.org/ApplicationSchema#>
PREFIX geo: <http://marklogic.com/geospatial#>
PREFIX cts: <http://marklogic.com/cts#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
 
SELECT *
WHERE { ?s my:hasExactGeometry ?o
FILTER geof:sfCoveredBy(?o,?california)
BIND (geof:distance(geo:approx-center(?o),'POINT(-121.49580152307632 38.577076831458754)','units=miles') AS ?centroidDistance)
}
`
 
sem.sparql(query,wktLiterals);

The result of the above query answers the question: “Which triple objects have regions in California, and what are the distances from the approximate center of each region to Sacramento, California?”

First, we find the region objects that are covered by our California WKT literal. Then, we approximate the center of the results and pass each to geof:distance() to find out how far the center of our region object is from Sacramento, represented by the WKT point ‘POINT(-121.49580152307632 38.577076831458754)’.

Notice you can call useful built-in functions in your SPARQL queries, e.g. geo:approx-center(). See Related Resources for more callable functions.

To continue with this introduction to Optic Geo, we can use some open-source tools to look at the data that we’ve stored in MarkLogic on an interactive map. Check out Using QGIS to View Geospatial Data for a walk-through of using QGIS 3, a popular open-source GIS tool.

To continue learning about Optic Geo, check out Advanced Optic Geo Topics.

Related Resources

This website uses cookies.

By continuing to use this website you are giving consent to cookies being used in accordance with the MarkLogic Privacy Statement.