Table Of Content
GET /
The API provides data about nodes and edges on a map. The node is a measurement station, and the connection between stations are called edges. The first path variable is the representation.
For compactness, we will use only flat representations throughout this tutorial.
Examples:
GET /tree,node/
GET /flat,node/
GET /tree,edge/
GET /flat,edge/
GET /tree,event/
GET /flat,event/
We expose all kinds of events, that are labels on the timeline. An event has a
unique ID and corresponding time series UUID, which allows us to concatenate
events into a historical view. Events come from a certain origin
, for example,
traffic events from the A22
highway. The prefixes of events are ev
for the
general information part, and evl
for its location (could be points, polygons
or multilines).
GET /flat,event/
GET /flat,event/A22
GET /flat,event/A22/latest
GET /flat,event/A22/2022-01-03
GET /flat,event/A22/latest/2022-01-03/2022-02-01
We expose only available edges, but for historical reasons the eavailable
,
sbavailable
and seavailable
fields are still accessible. Availability should
have been an internal-only field to mark a station visible through the API.
An edge is a connection between two stations and some descriptive fields attached. In addition it contains geometries, that describe the connection on a map.
An edge is (for historical reasons) internally represented with three stations, a start station, an end station and a station that represents the description of the edge. Therefore we have three prefixes of JSON fields:
e
are part of the edge descriptionssb
are part of the beginning stationse
are part of the ending stationGET /flat,edge/Linkstation
/flat,edge/LinkStation?where=ename.eq."tis -> cfirmiano"
We expose only available stations, but for historical reasons the
savailable
fields are still accessible. Availability should have been an
internal-only field to mark a station visible through the API.
Please note, that the response is limited. However, you can set another limit or disable it completely.
GET /node,flat/EChargingStation
is the same as
GET /flat/EChargingStation
GET /flat/EChargingStation,EChargingPlug
As you see an EChargingStation
is a parent of EchargingPlug
s, hence we could
avoid duplicate output, by simply fetching only plugs.
GET /flat/EChargingPlug
GET /flat/*
GET /flat/ParkingStation/*/latest
GET /flat/ParkingStation/occupied/latest
The URL pattern is /station-types/data-types/from/to
, where from
and to
form a half-open interval, i.e., [from, to)
. This is important, if we want to
have a moving window over a timeline without selecting certain values multiple
times.
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02
GET /flat/ParkingStation/occupied/2019-01-01T23/2019-01-02
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02T12:30:15
The date format is yyyy-MM-dd
or yyyy-MM-ddThh:mm:ss.SSS
, where
Thh:mm:ss.SSS
is optional and any part of it can be shortened from
left-to-right to any subset.
The URL pattern is /station-types/metadata/from/to
, where from
and to
form a half-open interval, i.e., [from, to)
.
Note that while metadata looks like a data type in the URL, the data structure is actually different from normal data requests. Current metadata is still included as smetadata, historical metadata has the prefix "mh"
GET /tree,node/BluetoothStation/metadata/2019-01-01/2023-01-02
GET /tree,node/BluetoothStation/metadata/2019-01-01T23/2023-01-02
GET /flat/BluetoothStation/metadata/2019-01-01/2023-01-02T12:30:15
The date format is yyyy-MM-dd
or yyyy-MM-ddThh:mm:ss.SSS
, where
Thh:mm:ss.SSS
is optional and any part of it can be shortened from
left-to-right to any subset.
You can limit your output by adding limit
to your request, and paginate your
results with an offset
. If you want to disable the limit, set it to a negative
number, like limit=-1
. Per default, the limit is set to a low number to
prevent excessive response times.
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02?limit=100&offset=300
It is possible to filter against JSON fields (columns in a database) with
select=alias,alias,alias,...
, or per record (rows in a database) with
where=filter,filter,filter,...
. The latter, is a conjunction (and
) of all
clauses. Also complex logic is possible, with nested or(...)
and and(...)
clauses, for instance where=or(filter,filter,and(filter,filter))
.
alias
An alias
is a list of point-separated-fields, where each field corresponds
to a step inside the JSON hierarchy. Internally, the first field represents the
database column and all subsequent fields drill into the JSON hierarchy.
For example, metadata.municipality.cap
is an JSONB inside the database with a
column metadata
and a JSONB object called municipality
which has a cap
inside.
filter
A filter
has the form alias.operator.value_or_list
.
value_or_list
value
: Whatever you want, also a regular expression. Use double-quotes to
force string recognition. Alternatively, you can escape characters ,
, '
and "
with a \
. Use url-encoding, if your tool does not support certain
characters. Special values are null
, numbers and omitted values. Examples:
description.eq.null
, checks if a description is not setdescription.eq.
, checks if a description is a string of length 0list
: (value,value,value)
operator
eq
: Equalneq
: Not Equallt
: Less Thangt
: Greater Thanlteq
: Less Than Or Equalgteq
: Greater Than Or Equalre
: Regular Expressionire
: Insensitive Regular Expressionnre
: Negated Regular Expressionnire
: Negated Insensitive Regular Expressionbbi
: Bounding box intersecting objects (e.g. a street that is only partially
covered by the box)bbc
: Bounding box containing objects (e.g. a station or street, that is
completely covered by the box)dlt
: Within distance from point (e.g. all stations within a 5 km radius from point X)in
: True, if the value of the alias can be found within the given list.
Example: name.in.(Patrick,Rudi,Peter)
nin
: False, if the value of the alias can be found within the given list.
Example: name.nin.(Patrick,Rudi,Peter)
logical operations
and(filter,filter,...)
: Conjunction of filters (can be nested)or(filter,filter,...)
: Disjunction of filters (can be nested)Multiple conditions possible as comma-separated-values.
Example-syntax for bbi
or bbc
could be coordinate.bbi.(11,46,12,47,4326)
, where
the ordering inside the list is left-x, left-y, right-x, right-y and SRID
(optional).
NB: Currently it is not possible to distinguish between a JSON field containing null
or a non-existing JSON field.
You can use any SQL function within select, which takes only a single numeric value. All selected aliases, that are not within a function are used for grouping.
Example: I want to have the min
, max
, avg
and count
of all data types of
e-charging stations.
GET /flat/EChargingStation/*?select=tname,min(mvalue),max(mvalue),avg(mvalue),count(mvalue)
NB: Currently only numeric functions are possible, we will not select anything from our string measurements.
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02?select=sname,tname,mvalue
GET /flat/ParkingStation/*?where=scoordinate.bbi.(11.63,46.0,11.65,47.0,4326)
... I want now to add to that query two additional stations (ex., 69440GW and AB3), that I need regardless, if they are within the bounding box or not.
GET /flat/ParkingStation/*?where=or(scoordinate.bbi.(11.63,46.0,11.65,47.0,4326),scode.in.(69440GW,AB3))
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02?where=mvalue.gt.100,sorigin.eq.FAMAS
Here the syntax for each clause is attribute.operator.value
, where value can
be composed of any character except ,'"
, which must be escaped like \,
, \'
or \"
. A special value is null
. If you want to use it as a literal value,
that is, the String itself, then you must put it into double-quotes, like
"null"
.
We use a JSON selector and JSON filters here:
GET /flat/CreativeIndustry?where=smetadata.sector.eq.null&select=sname
We use a key-insensitive regular expression here:
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02?where=scode.ire.(ME|Rovereto)
We use a JSON selector and JSON filters here:
GET /flat/CreativeIndustry?where=sactive.eq.true,smetadata.website.neq.null,smetadata.website.ire."http"&select=sname,smetadata.sector,smetadata.website
We check not only for smetadata.website
to be present, but also to start with http
to be sure it
is not a description telling us, that the website is currently under development or similar things.
We use UTC as default time zone, but it is now possible to get timestamp
reponses in any timezone. Use timezone=Europe/Rome
for instance. See
java.time.ZoneId
for details. If the browser replaces +
with spaces, the API tries to insert
+
again, and searches then for matching zone IDs.
GET /flat/ParkingStation/occupied/latest?timezone=UTC-2
Please note, that metadata
are kept as is, date or time representations inside it are stored as simple
strings, and therefore not recognized as date or time.
You can also see null-values within JSON, by adding shownull=true
to your parameter list.
GET /flat/ParkingStation/occupied/2019-01-01/2019-01-02?shownull=true
We have various types of representations to choose from. Separate each type with commas:
flat
or tree
node
, edge
or event
(node
is the default and can be omitted)The flat one shows each JSON object with all selected attributes at the first
level. Deeper levels represent complex data types, such as coordinates
and
jsonb
. Only the first level can be selected or filtered.
Example with select=stype,tname,mvalue,smetadata
:
{
"data": [
{
"stype": "ParkingStation",
"tname": "occupied",
"mvalue": 300,
"smetadata": {
"capacity": 1200,
"...": "..."
}
}
]
}
As you can see, the station type stype
and the data type tname
are on the
same level within the JSON object. These are first order attributes, whereas
smetadata
is a jsonb
-typed column.
If you want to retrieve only subsets of information, like all data types
,
which do not match inside a hierarchy, this representation is suited for you.
{
"data": [
{
"tname": "ParkingStation"
},
{
"tname": "VMS"
},
{
"tname": "EChargingStation"
}
]
}
The tree
representation, shows a hierarchy of the following kind for nodes:
station types / categories
└── stations (incl. parent and metadata)
├── data types
│ └── measurements
└── metadatahistory
...and the following hierarchy for edges:
edge types / categories
└── edges (incl. start and end station)
...whereas, events have this hierarchy:
event origins
└── event series uuids
└── event uuids
NB: The tree
is more expensive to generate on the server and to use within
your application, but the response size can be much smaller due to nesting and
thus duplicate attribute elimination. However, some queries do not match that
hierarchy, so the flat
representation is more suited for them.
It is possible to configure a maximum request per second quota, depending on various constraints. Currently, we support these quota profiles:
name | description | .properties key |
---|---|---|
Anonymous | not logged in and no referer header | ninja.quota.guest |
Referer | referer header send | ninja.quota.referer |
Basic | Bearer Token containing a BASIC role, or no role at all | ninja.quota.basic |
Advanced | Bearer Token containing a ADVANCED role | ninja.quota.advanced |
Premium | Bearer Token containing a PREMIUM role | ninja.quota.premium |
Admin | Bearer Token containing the ADMIN role | no key / no quota |
In addition, the ninja.quota.url
property should contain a link to a
webpage, that
explains what the 429
HTTP error code means.
Roles must be set as follows in Keycloak:
1) Open your Keycloak server
2) Go to clients and open odh-mobility-v2
in your clients section
3) Under Roles
add these roles:
ODH_ROLE_BASIC
: Open Data Hub Pricing Policy: Basic (if logged in and
roles are missing, this is the DEFAULT)ODH_ROLE_ADVANCED
: Open Data Hub Pricing Policy: AdvancedODH_ROLE_PREMIUM
: Open Data Hub Pricing Policy: PremiumODH_ROLE_ADMIN
: Open Data Hub Pricing Policy: Administrator = no
restrictions at all (only used internally)The role prefix is ODH_ROLE_
for quota related roles, and BDP_
for
row-level-security or open/closed data definition roles.
There should be at least one row-level-security role called BDP_ADMIN
, this
role should be a composite role with ODH_ROLE_ADMIN
. This means, that if one
is activated also the other one will be activated. BDP_ADMIN
implies that this
role can see all data, and ODH_ROLE_ADMIN
removes all quota restrictions from
those calls.
See How to register this application in your local authentication server? for further details.
We limit rates through a token-bucket algorithm. We identify a rate-limit bucket with keys build as explained inside the method RateLimitInterceptor/resolveBucket.
We use a token based authentication (JWT) which can be retrieved from an OAuth 2.0 server.
NB: Swagger does not support authentication yet, therefore we provide a curl
example.
curl -X GET "https://example.com/tree/VMS/*" \
-H 'Authorization: Bearer header.payload.signature'
For better readability, we assume that all queries are configured as follows, if
not otherwise stated: shownull=false&distinct=true&limit=-1
.
NB: We need to count results on application level, because the API does
currently not support aggregation, like count
and other statistical methods
involving grouping
.
GET /flat/EChargingStation?where=sactive.eq.true,scoordinate.bbi.(11.27539,46.444913,11.432577,46.530384)
Since we want to count the results later, we need to set distinct=false
.
GET /flat/EChargingStation?select=smetadata.accessType&where=sactive.eq.true&distinct=false
This means that the measured value mvalue
must be equal 1
.
GET /flat/EChargingPlug/*?select=scode&where=sactive.eq.true,tname.eq.echarging-plug-status,mvalue.eq.1
GET /flat/EChargingPlug?where=sactive.eq.true
GET /flat/EChargingStation?where=sactive.eq.true,smetadata.accessType.eq.PUBLIC
GET /flat/EChargingStation?select=smetadata.state
For example filter against ACTIVE
states.
GET /flat/EChargingPlug?where=sactive.eq.true,smetadata.state.eq.ACTIVE
Create local
application properties profile.
cd src/main/resources
touch application-local.properties
Configure at least the mandatory properties in the newly created application-local.properties
file, such as:
Now you can start the application with:
mvn spring-boot:run -Dspring-boot.run.profiles=local
The server will startup and listen on http://localhost:8081
.
Property | Value |
---|---|
ClientID | odh-mobility-v2 |
Property | Value |
---|---|
Access Type | bearer-only |
Add following roles: BDP_ADMIN, BDP_BLC, BDP_MAD, BDP_CBZ
Property | Value |
---|---|
ClientID | odh-mobility-client |
Property | Value |
---|---|
Access Type | public |
Standard Flow Enabled | Off |
Implicit Flow Enabled | Off |
Direct Access Grants Enabled | On |
Property | Value |
---|---|
Full Scope Allowed | Off |
Client Roles -> odh-mobility-v2 -> Assigned Roles | Move available roles to assigned roles |
curl --location --request POST 'http://localhost:8080/auth/realms/noi/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username={USERNAME}' \
--data-urlencode 'password={PASSWORD}' \
--data-urlencode 'client_id=odh-mobility-client'
This project is REUSE compliant, more information about the usage of REUSE in NOI Techpark repositories can be found here.
Since the CI for this project checks for REUSE compliance you might find it useful to use a pre-commit hook checking for REUSE compliance locally. The pre-commit-config file in the repository root is already configured to check for REUSE compliance with help of the pre-commit tool.
Install the tool by running:
pip install pre-commit
Then install the pre-commit hook via the config file by running:
pre-commit install