Skip to content

Geographic components

Invenio Geographic Components is a React library providing high-level geographic components for InvenioRDM instances, allowing users to easily create and visualize Locations. To facilitate the installation and configuration of the components, a Python library is also provided, with all the assets required to install the components in an InvenioRDM instance.

The usage of the Python library in an InvenioRDM instance doesn’t increase the build time, as the Python package already includes pre-compiled versions of the React components.

To start, you first need to install the Python distribution in your instance. For this, you can use your favorite package manager. Assuming you are using the default pipenv from InvenioRDM, you can add the package to the Pipfile of your instance as follows:

[packages]
invenio-geographic-components = {git = "https://github.com/geo-knowledge-hub/invenio-geographic-components-react.git", subdirectory = "python"}

Then install it and rebuild the web assets:

Terminal window
invenio-cli install
invenio-cli assets build

The two steps serve different parts of the extension. The landing page map is served by the extension’s own blueprint, already built, so it works right after the install. The deposit form fields are exposed as a webpack alias, so they only become importable once the assets are rebuilt.

The extension is configured in two places. The landing page map is configured in invenio.cfg, and the deposit form field is configured through the props of the React component you place in your form.

There are two instance configurations available, which contain sensible default values you can update in your invenio.cfg file. Below, these variables are presented, as well as the default values used in them.

The first variable available is GEOGRAPHIC_COMPONENTS_MAP_CONFIG, which allows you to configure the map presented on the Record Landing Page:

GEOGRAPHIC_COMPONENTS_MAP_CONFIG = {
"mapContainer": {"center": [0, 0], "zoom": 2, "scrollWheelZoom": False},
"fitBoundsOptions": {"maxZoom": 12},
"useTileLayers": True,
"useFullscreen": True,
"useGeocoding": False,
"useMouseCoordinate": False,
}

These default values are used to control various things, ranging from map zoom to the addition of geocoding and mouse coordinates on the map. The description of all parameters available is presented below:

KeyDefaultEffect
mapContainercenter [0, 0], zoom 2, no scroll wheel zoomLeaflet map options. They apply until the map fits itself to the record geometries.
fitBoundsOptionsmaxZoom 12Limits how far the map zooms when it fits the geometries. Without it a single point zooms to street level.
useTileLayersTrueBase map switcher, with a credits button per base map.
useFullscreenTrueFullscreen button.
useGeocodingFalseAddress search box.
useMouseCoordinateFalseCursor coordinates readout.

The second variable available is GEOGRAPHIC_COMPONENTS_TILE_HOSTS, which allows you to control the tile hosts allowed in your instance. By default, the Esri, OpenStreetMap and OpenTopoMap services are allowed:

GEOGRAPHIC_COMPONENTS_TILE_HOSTS = [
"https://server.arcgisonline.com",
"https://*.tile.openstreetmap.org",
"https://*.tile.opentopomap.org",
]

Apart from the component used to show Locations on the Record Landing Page, Invenio Geographic Components provides the LocationsField, a Field component you can add to your InvenioRDM form:

<LocationsField
fieldPath="metadata.locations.features"
/>

This component contains the following props:

PropDefaultEffect
fieldPathmetadata.locationsWhere the locations are stored in the form state.
labelGeographic LocationsField heading.
labelIconglobeIcon shown next to the heading.
requiredfalseMarks the field required in the form.
locationAddButtonLabelAdd locationLabel of the button that opens the dialog.
modalConfigaddLabel, editLabelTitles of the add and edit dialogs.
interactiveMapConfig{}Options for the map inside the dialog.
uniqueLayerfalseWhen enabled, a new drawing replaces the previous one, so a location always holds exactly one shape.
geometryTypesPoint, MultiPoint, PolygonThe geometry types the instance accepts.

The LocationsField allows users to use an interactive map to define geometries, as well as to import GeoJSON files. These operations use a map inside the field, which can be configured using the interactiveMapConfig prop.

This interactiveMapConfig is passed straight to the map component, so the map options sit under a mapConfig key. That object replaces the default instead of merging with it, which means mapContainer has to be included whenever you set it:

<LocationsField
fieldPath="metadata.locations.features"
interactiveMapConfig={{
mapConfig: {
mapContainer: { center: [0, 0], zoom: 2, scrollWheelZoom: false },
useTileLayers: true,
useFullscreen: true,
useGeocoding: false,
useMouseCoordinate: false,
},
}}
/>

The editor map enables all four controls by default (i.e., useTileLayers, useFullscreen, useGeocoding, and useMouseCoordinate), while the landing page map leaves the address search and the coordinates readout off. Set them explicitly if you want the two maps to match.

The drawing tools live under the same object, in geometryEditorConfig.toolbarConfig, and accept the Geoman toolbar options. By default the toolbar offers marker, rectangle, polygon, edit, drag, delete and rotate. The line tool is not offered, because InvenioRDM rejects a LineString on save.

A Location can only hold one geometry, so when we draw a second geometry, the component can’t create a second geometry. Instead, it is merged and transformed into a Multi type. For cases where we mix multiple types of geometries, the component creates a GeometryCollection.

However, default InvenioRDM instances only store Point, MultiPoint and Polygon, rejecting the rest on save with Unsupported value: <type>. But there are also instances which support more geometry types, as is the case with the GEO Knowledge Hub.

To support both cases, the LocationsField allows users to configure which geometryTypes users are allowed to use. By default, Point, MultiPoint and Polygon are allowed, but if required, it is possible to change them:

<LocationsField
fieldPath="metadata.locations.features"
geometryTypes={["Point", "MultiPoint", "Polygon", "MultiPolygon"]}
/>

The deposit form is a stack of accordion sections, with Basic information, Recommended information, Funding, and so on. Adding the LocationsField to one of them, the way the walkthrough below does, places it in different sections, among different fields. That reads well when locations are a detail of the record, and less well when they are one of its subjects and important information in the metadata that needs to be visible for users to easily add it.

For those cases the library offers the LocationsAccordion. It creates an accordion already set up for the locations, with a proper title and icon for the locations, as well as the field itself in it. The component is based on the AccordionField from the InvenioRDM components, so the section sits naturally among the ones the form already has.

<LocationsAccordion geometryTypes={["Point", "MultiPoint", "Polygon"]} />

To place it, you override one of the accordions the form already has. The override renders that accordion back as it was, and puts the locations before or after it.

For example, to have the locations between Recommended information and Funding, override Funding and let the locations come first:

import { AccordionField } from "react-invenio-forms";
import { LocationsAccordion } from "@js/invenio_geographic_components";
import "@js/invenio_geographic_components/index.css";
function FundingWithLocations({ children, ...accordionProps }) {
return (
<>
<LocationsAccordion geometryTypes={["Point", "MultiPoint", "Polygon"]} />
<AccordionField {...accordionProps}>{children}</AccordionField>
</>
);
}
export const overriddenComponents = {
"InvenioAppRdm.Deposit.AccordionFieldFunding.container": FundingWithLocations,
};

Overriding any other accordion works the same way.

The LocationsAccordion uses the props below. Everything else goes to the LocationsField inside it, so the field options from the previous sections are set here directly:

PropDefaultEffect
fieldPathmetadata.locations.featuresWhere the locations are stored in the form state.
labelGeographic LocationsSection title.
idlocations-sectionId of the section on the page. The form’s error summary links to it.
activetrueWhether the section starts open.
includesPathsthe fieldPathForm paths whose errors the section counts in its header.
severityChecksNoneLabels for those counts, as your instance defines them.

To use the component in an InvenioRDM instance, there are only a few steps you need to follow. These steps are specified below:

  1. Install the package. First, add the package in your Pipfile, then install and bundle it:

    # Pipfile
    [packages]
    invenio-geographic-components = {git = "https://github.com/geo-knowledge-hub/invenio-geographic-components-react.git", subdirectory = "python"}
    Terminal window
    invenio-cli install # install and bundle!
  2. Configure the Record Landing Page Sidebar. To show the Locations available in the metadata on the Record Landing Page, you must configure in your invenio.cfg the usage of the sidebar template provided by the package. For this, you can swap the built-in sidebar section for the one the package provides:

    from invenio_app_rdm.config import APP_RDM_DETAIL_SIDE_BAR_TEMPLATES as _SIDE_BAR
    APP_RDM_DETAIL_SIDE_BAR_TEMPLATES = [
    "invenio_geographic_components/records/details/side_bar/locations_map.html"
    if template.endswith("side_bar/locations.html")
    else template
    for template in _SIDE_BAR
    ]
  3. Allow the base map services. InvenioRDM only permits same-origin images, so the tile servers have to be added to the Content Security Policy. Place this after APP_DEFAULT_SECURE_HEADERS is defined.

    from invenio_geographic_components.config import GEOGRAPHIC_COMPONENTS_TILE_HOSTS
    APP_DEFAULT_SECURE_HEADERS["content_security_policy"]["img-src"] = [
    "'self'",
    "data:",
    "blob:",
    *GEOGRAPHIC_COMPONENTS_TILE_HOSTS,
    ]
  4. Add the field to the deposit form. To include the LocationsField, without cloning the InvenioRDM deposit form, we can use the overridable component mechanism provided by InvenioRDM. For this, you can include the following content in the assets/js/invenio_app_rdm/overridableRegistry/mapping.js in your instance:

    import React from "react";
    import PropTypes from "prop-types";
    import { AccordionField } from "react-invenio-forms";
    import { LocationsField } from "@js/invenio_geographic_components";
    import "@js/invenio_geographic_components/index.css";
    const LOCATIONS_PATH = "metadata.locations.features";
    function RecommendedInformationWithLocations({ children, includesPaths, ...props }) {
    return (
    <AccordionField {...props} includesPaths={[...includesPaths, LOCATIONS_PATH]}>
    {children}
    <LocationsField fieldPath={LOCATIONS_PATH} label="Locations" labelIcon="globe" />
    </AccordionField>
    );
    }
    RecommendedInformationWithLocations.propTypes = {
    children: PropTypes.node.isRequired,
    includesPaths: PropTypes.arrayOf(PropTypes.string).isRequired,
    };
    export const overriddenComponents = {
    "InvenioAppRdm.Deposit.AccordionFieldRecommendedInformation.container":
    RecommendedInformationWithLocations,
    };

    An override receives the props of the component it replaces, not the component itself, so children are the fields the section already renders and the section is rebuilt around them.

  5. Emit the stylesheet of that bundle. The import above puts the styles in the overridable-registry entry, and InvenioRDM emits that entry’s JavaScript but not its CSS. Add templates/semantic-ui/<your-instance-id>/records/deposit.html:

    {%- extends "invenio_app_rdm/records/deposit.html" %}
    {%- block css %}
    {{ super() }}
    {{ webpack['overridable-registry.css'] }}
    {%- endblock %}

    and point the deposit form at it in invenio.cfg:

    APP_RDM_DEPOSIT_FORM_TEMPLATE = "<your-instance-id>/records/deposit.html"
  6. Rebuild and run.

    Terminal window
    invenio-cli assets build
    invenio-cli run

    The locations field appears at the bottom of Recommended information on the deposit form, and the map appears in the sidebar of any published record that has a drawable location.

The base maps need the tile hosts allowed. Without step 3 the map renders and stays blank, since the browser blocks the tiles. The hosts are exported as GEOGRAPHIC_COMPONENTS_TILE_HOSTS, so a custom base map needs its own host added to the same list.

On InvenioRDM v13 drafts only one location may carry a geometry. A record may hold any number of locations, but saving a draft with a second geometry fails with an OpenSearch error:

DocValuesField "metadata.locations.features.geometry" appears more than once
in this document (only one value is allowed per field)

The drafts index mapping shipped by invenio-rdm-records in InvenioRDM v13 leaves doc_values on for that field, while the published records mapping disables them, and OpenSearch allows one geo_shape doc values entry per document. Locations without a geometry are unaffected. This is fixed upstream in invenio-rdm-records#2397, which is on master and has not been backported to the maint-19.x branch that v13 installs. Until it is, an instance that needs several geometries has to correct the mapping itself:

invenio_rdm_records/records/mappings/os-v2/rdmrecords/drafts/draft-v6.0.0.json
"geometry": { "type": "geo_shape" }
"geometry": { "type": "geo_shape", "doc_values": false }

doc_values cannot be changed on an existing index, so the drafts index has to be recreated afterwards.

The identifiers picker needs a vocabulary API. The dialog offers a geographic identifiers field that searches /api/geoidentifiers, an endpoint stock InvenioRDM does not provide. It comes from invenio-geographic-identifiers. The endpoint is not configurable from LocationsField, so on an instance without that package the field has nothing to search, while the rest of the dialog works normally.

The repository holds two projects. The React components live at the root, with the sources in src/, and the Python distribution lives in python/. The build compiles the sources into the Python package, which is why an instance never builds the JavaScript itself.

There are two bundles, and they match the two halves of the install:

SourceOutputServes
src/viewer/static/, in the Python packageThe landing page map, delivered by the extension’s blueprint.
src/lib/assets/, in the Python packageThe deposit form components, exposed through the webpack alias.

Both outputs are committed, so a change to the sources is only complete once the bundles are rebuilt and committed with it. The continuous integration checks this and fails when the two have drifted.

Terminal window
git clone https://github.com/geo-knowledge-hub/invenio-geographic-components-react.git
cd invenio-geographic-components-react
npm install
npm run build

npm test runs the component tests, and npm run watch rebuilds on save while you work.

The Python package is installed from its own directory. The script beside it rebuilds the bundles, checks the distribution manifest and runs the tests, so it is the single command to run before opening a pull request:

Terminal window
cd python/
pip install -e ".[tests]"
./run-tests.sh

A Storybook covering the components on this page is published at geo-knowledge-hub.github.io/invenio-geographic-components-react, including the locations field, the geometry editor, the GeoJSON importer and the landing page viewer. Each story is interactive, with the component props exposed as controls, which is the quickest way to see what a configuration does before wiring it into an instance.

If you are working on the components themselves, the same playground runs from a checkout of the repository:

Terminal window
npm run storybook

It opens on http://localhost:6006. npm run storybook-build produces a static copy in storybook-static/ if you prefer to serve it yourself.

Invenio Geographic Components is distributed under the MIT license. See LICENSE for the full text.