Skip to content

Wells on a map

Every well you may read in a project can have a location: the newest source you may read that locates it — a row of a table of wells, or a well-location file associated with the well. The location is never a second copy of the data: it is read from that source, in the CRS it was stored in, and converted only when you ask for another CRS. Each converted location records the conversion that was performed.

Ophiolite serves the wells two ways:

  • as JSON, from entities/list (with bbox, bbox_crs and crs) and entities/extent, and through the Python SDK’s wells() and extent();
  • as an OGC API – Features collection that GIS tools open directly: /api/v1/projects/<project>/features/collections/wells/items.

Both apply the same rule: a location counts only when you may read its source, so a box never reveals a well you may not see, and a well whose source was withdrawn falls back to an older source you may read.

Ask for the extent in CRS84 (longitude, latitude), which is what web maps use:

Terminal window
curl -s -X POST https://ophiolite.example/api/v1/projects/my-project/entities/extent \
-H "Authorization: Bearer $OPHIOLITE_ACCESS_KEY" -H 'Content-Type: application/json' \
-d '{"project_id": "my-project", "crs": "OGC:CRS84"}'
# {"crs": "OGC:CRS84", "bbox": [5.12, 52.01, 6.93, 53.47], "count": 212, "untransformed": 0}

untransformed counts wells whose stored CRS cannot be converted; they are left out of a converted extent rather than placed wrongly.

The OGC items response is GeoJSON in CRS84 by default, so MapLibre can use it as a source. Show each well’s name in its popup; keep identifiers, revisions and the source under a “Technical details” disclosure:

// Popup content: the well's name first; identifiers only under Technical details.
function wellPopup(feature) {
const p = feature.properties;
const root = document.createElement("div");
const name = document.createElement("strong");
name.textContent = p.name;
root.append(name);
const details = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = "Technical details";
const list = document.createElement("dl");
for (const [label, value] of [["Well", p.entity_id], ["Source", p.source_asset_id], ["Revision", p.source_revision], ["Row", p.source_row]]) {
if (!value) continue;
const term = document.createElement("dt"); term.textContent = label;
const text = document.createElement("dd"); text.textContent = value;
list.append(term, text);
}
details.append(summary, list);
root.append(details);
return root;
}
const base = "https://ophiolite.example/api/v1/projects/my-project";
const headers = { Authorization: "Bearer " + accessKey };
const extent = await (await fetch(base + "/entities/extent", {
method: "POST", headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ project_id: "my-project", crs: "OGC:CRS84" }) })).json();
const wells = await (await fetch(base + "/features/collections/wells/items?limit=100", { headers })).json();
map.on("load", () => {
map.addSource("wells", { type: "geojson", data: wells });
map.addLayer({ id: "wells", type: "circle", source: "wells", paint: { "circle-radius": 5, "circle-color": "#2f6f5e" } });
map.on("click", "wells", (e) => new maplibregl.Popup().setLngLat(e.lngLat).setDOMContent(wellPopup(e.features[0])).addTo(map));
if (extent.bbox) map.fitBounds([[extent.bbox[0], extent.bbox[1]], [extent.bbox[2], extent.bbox[3]]], { padding: 40 });
});

Follow the response’s next link for more than 100 wells. A well without a location you may read has a null geometry and is not drawn.

The JSON route needs the box’s CRS explicitly; nothing is guessed from the data:

Terminal window
curl -s -X POST https://ophiolite.example/api/v1/projects/my-project/entities/list \
-H "Authorization: Bearer $OPHIOLITE_ACCESS_KEY" -H 'Content-Type: application/json' \
-d '{"project_id": "my-project", "bbox": [150000, 460000, 170000, 480000], "bbox_crs": "EPSG:28992", "crs": "OGC:CRS84"}'

The OGC route takes bbox and bbox-crs (default CRS84) and returns coordinates in crs (default CRS84), each in its CRS’s axis order — EPSG:4326 is latitude first. The supported CRSs are CRS84, EPSG:4326, EPSG:3857, RD New (EPSG:28992) and UTM zone 31N (EPSG:32631); another is refused with 400.

In Python:

wells = client.wells(bbox=[150000, 460000, 170000, 480000], bbox_crs="EPSG:28992", crs="OGC:CRS84")
wells.to_frame() # name, x, y, crs and the source of each location
wells.to_geojson() # needs the CRS84 fetch above; nothing is converted locally
client.extent() # the same extent as entities/extent

In QGIS choose Layer › Add Layer › Add WFS / OGC API – Features Layer, create a connection to https://ophiolite.example/api/v1/projects/my-project/features with your access key as a bearer token, and add the wells collection.

Coordinates are converted between the listed CRSs only; see Coordinates for how other data types are routed.