Merge pull request #6 from jdejaegh/rio_ifdm

Implement RIO IFDM client
This commit is contained in:
Jules 2024-06-30 18:26:33 +02:00 committed by GitHub
commit 86addac7cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 951 additions and 154 deletions

View file

@ -1,6 +1,6 @@
from .api import IrcelineApiError from .api import IrcelineApiError
from .rio import IrcelineRioClient from .data import RioFeature, ForecastFeature, FeatureValue, RioIfdmFeature
from .forecast import IrcelineForecastClient from .forecast import IrcelineForecastClient
from .data import RioFeature, ForecastFeature, FeatureValue, BelAqiIndex from .rio import IrcelineRioClient, IrcelineRioIfdmClient
__version__ = '2.0.0' __version__ = '2.0.0'

View file

@ -2,17 +2,17 @@ import asyncio
import socket import socket
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Tuple, List, Set from typing import Tuple, List, Set
from xml.etree import ElementTree
import aiohttp import aiohttp
import async_timeout import async_timeout
from aiohttp import ClientResponse
from .data import IrcelineFeature from .data import IrcelineFeature
from .utils import SizedDict
_rio_wfs_base_url = 'https://geo.irceline.be/wfs' _rio_wfs_base_url = 'https://geo.irceline.be/wfs'
_forecast_wms_base_url = 'https://geo.irceline.be/forecast/wms' _forecast_wms_base_url = 'https://geo.irceline.be/forecast/wms'
# noinspection HttpUrlsUsage _rio_ifdm_wms_base_url = 'https://geobelair.irceline.be/rioifdm/wms'
# There is not HTTPS version of this endpoint
_user_agent = 'github.com/jdejaegh/python-irceline' _user_agent = 'github.com/jdejaegh/python-irceline'
@ -21,9 +21,8 @@ class IrcelineApiError(Exception):
class IrcelineBaseClient(ABC): class IrcelineBaseClient(ABC):
def __init__(self, session: aiohttp.ClientSession, cache_size: int = 20) -> None: def __init__(self, session: aiohttp.ClientSession) -> None:
self._session = session self._session = session
self._cache = SizedDict(cache_size)
@abstractmethod @abstractmethod
async def get_data(self, async def get_data(self,
@ -65,3 +64,38 @@ class IrcelineBaseClient(ABC):
raise IrcelineApiError(f"Something really wrong happened! {exception}") from exception raise IrcelineApiError(f"Something really wrong happened! {exception}") from exception
class IrcelineBaseWmsClient(IrcelineBaseClient, ABC):
_default_querystring = {"service": "WMS",
"version": "1.1.1",
"request": "GetFeatureInfo",
"info_format": "application/json",
"width": "1",
"height": "1",
"srs": "EPSG:4326",
"X": "1",
"Y": "1"}
_epsilon = 0.00001
_base_url = None
@staticmethod
def _parse_capabilities(xml_string: str) -> Set[str]:
try:
root = ElementTree.fromstring(xml_string)
except ElementTree.ParseError:
return set()
path = './/Capability/Layer/Layer/Name'
feature_type_names = {t.text for t in root.findall(path)}
return feature_type_names
async def get_capabilities(self) -> Set[str]:
"""
Fetch the list of possible features from the WMS server
:return: set of features available on the WMS server
"""
querystring = {"service": "WMS",
"version": "1.1.1",
"request": "GetCapabilities"}
r: ClientResponse = await self._api_wrapper(self._base_url, querystring)
return self._parse_capabilities(await r.text())

View file

@ -1,5 +1,5 @@
from datetime import datetime, date from datetime import datetime, date
from enum import StrEnum, Enum from enum import StrEnum
from typing import TypedDict from typing import TypedDict
@ -30,6 +30,14 @@ class RioFeature(IrcelineFeature):
SO2_HMEAN = 'rio:so2_hmean' SO2_HMEAN = 'rio:so2_hmean'
class RioIfdmFeature(IrcelineFeature):
PM25_HMEAN = 'rioifdm:pm25_hmean'
NO2_HMEAN = 'rioifdm:no2_hmean'
PM10_HMEAN = 'rioifdm:pm10_hmean'
O3_HMEAN = 'rioifdm:o3_hmean'
BELAQI = 'rioifdm:belaqi'
class ForecastFeature(IrcelineFeature): class ForecastFeature(IrcelineFeature):
NO2_MAXHMEAN = 'forecast:no2_maxhmean' NO2_MAXHMEAN = 'forecast:no2_maxhmean'
NO2_DMEAN = 'forecast:no2_dmean' NO2_DMEAN = 'forecast:no2_dmean'
@ -40,20 +48,7 @@ class ForecastFeature(IrcelineFeature):
BELAQI = 'forecast:belaqi' BELAQI = 'forecast:belaqi'
class BelAqiIndex(Enum):
EXCELLENT = 1
VERY_GOOD = 2
GOOD = 3
FAIRLY_GOOD = 4
MODERATE = 5
POOR = 6
VERY_POOR = 7
BAD = 8
VERY_BAD = 9
HORRIBLE = 10
class FeatureValue(TypedDict): class FeatureValue(TypedDict):
# Timestamp at which the value was computed # Timestamp at which the value was computed
timestamp: datetime | date | None timestamp: datetime | date | None
value: int | float | BelAqiIndex | None value: int | float | None

View file

@ -1,16 +1,16 @@
import asyncio
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from itertools import product from itertools import product
from typing import List, Tuple, Dict, Set from typing import List, Tuple, Dict
from xml.etree import ElementTree
from aiohttp import ClientResponse, ClientResponseError from aiohttp import ClientResponse, ClientResponseError
from .api import IrcelineBaseClient, _forecast_wms_base_url, IrcelineApiError from .api import IrcelineApiError, IrcelineBaseWmsClient, _forecast_wms_base_url
from .data import ForecastFeature, FeatureValue from .data import ForecastFeature, FeatureValue
class IrcelineForecastClient(IrcelineBaseClient): class IrcelineForecastClient(IrcelineBaseWmsClient):
_epsilon = 0.00001 _base_url = _forecast_wms_base_url
async def get_data(self, async def get_data(self,
features: List[ForecastFeature], features: List[ForecastFeature],
@ -26,50 +26,30 @@ class IrcelineForecastClient(IrcelineBaseClient):
timestamp = date.today() timestamp = date.today()
result = dict() result = dict()
lat, lon = position lat, lon = position
base_querystring = {"service": "WMS", base_querystring = (self._default_querystring |
"version": "1.1.1", {"bbox": f"{lon},{lat},{lon + self._epsilon},{lat + self._epsilon}"})
"request": "GetFeatureInfo",
"info_format": "application/json",
"width": "1",
"height": "1",
"srs": "EPSG:4326",
"bbox": f"{lon},{lat},{lon + self._epsilon},{lat + self._epsilon}",
"X": "1",
"Y": "1"}
for feature, d in product(features, range(4)): tasks = [asyncio.create_task(self._get_single_feature(base_querystring, d, feature, timestamp))
querystring = base_querystring | {"layers": f"{feature}_d{d}", for feature, d in product(features, range(4))]
"query_layers": f"{feature}_d{d}"} results = await asyncio.gather(*tasks)
try:
r: ClientResponse = await self._api_wrapper(_forecast_wms_base_url, querystring) for r in results:
r: dict = await r.json() result |= r
result[(feature, timestamp + timedelta(days=d))] = FeatureValue(
value=r.get('features', [{}])[0].get('properties', {}).get('GRAY_INDEX'),
timestamp=datetime.fromisoformat(r.get('timeStamp')) if 'timeStamp' in r else None)
except (IrcelineApiError, ClientResponseError, IndexError):
result[(feature, timestamp + timedelta(days=d))] = FeatureValue(value=None, timestamp=None)
return result return result
async def get_capabilities(self) -> Set[str]: async def _get_single_feature(self, base_querystring: dict, d: int, feature: ForecastFeature,
""" timestamp: date) -> dict:
Fetch the list of possible features from the WMS server result = dict()
:return: set of features available on the WMS server
"""
querystring = {"service": "WMS",
"version": "1.1.1",
"request": "GetCapabilities"}
r: ClientResponse = await self._api_wrapper(_forecast_wms_base_url, querystring)
return self._parse_capabilities(await r.text()) querystring = base_querystring | {"layers": f"{feature}_d{d}",
"query_layers": f"{feature}_d{d}"}
@staticmethod
def _parse_capabilities(xml_string: str) -> Set[str]:
try: try:
root = ElementTree.fromstring(xml_string) r: ClientResponse = await self._api_wrapper(self._base_url, querystring)
except ElementTree.ParseError: r: dict = await r.json()
return set() result[(feature, timestamp + timedelta(days=d))] = FeatureValue(
value=r.get('features', [{}])[0].get('properties', {}).get('GRAY_INDEX'),
path = './/Capability/Layer/Layer/Name' timestamp=datetime.fromisoformat(r.get('timeStamp')) if 'timeStamp' in r else None)
feature_type_names = {t.text for t in root.findall(path)} except (IrcelineApiError, ClientResponseError, IndexError):
return feature_type_names result[(feature, timestamp + timedelta(days=d))] = FeatureValue(value=None, timestamp=None)
return result

View file

@ -1,16 +1,21 @@
import asyncio
from datetime import datetime, date, UTC, timedelta from datetime import datetime, date, UTC, timedelta
from typing import List, Tuple, Dict, Set from typing import List, Tuple, Dict, Set
from xml.etree import ElementTree from xml.etree import ElementTree
from aiohttp import ClientResponse from aiohttp import ClientResponse, ClientResponseError
from .api import IrcelineBaseClient, _rio_wfs_base_url, IrcelineApiError from .api import IrcelineBaseClient, _rio_wfs_base_url, IrcelineApiError, _rio_ifdm_wms_base_url, IrcelineBaseWmsClient
from .data import RioFeature, FeatureValue from .data import RioFeature, FeatureValue, RioIfdmFeature
from .utils import epsg_transform from .utils import epsg_transform
class IrcelineRioClient(IrcelineBaseClient): class IrcelineRioClient(IrcelineBaseClient):
"""API client for RIO interpolated IRCEL - CELINE open data""" """
API client for RIO interpolated IRCEL - CELINE open data
RIO is more coarse grained for interpolation than RIO IFDM and allows to request multiple features in the same
request, which may be faster.
"""
async def get_data(self, async def get_data(self,
features: List[RioFeature], features: List[RioFeature],
@ -124,3 +129,48 @@ class IrcelineRioClient(IrcelineBaseClient):
result[name] = FeatureValue(timestamp=timestamp, value=value) result[name] = FeatureValue(timestamp=timestamp, value=value)
return result return result
class IrcelineRioIfdmClient(IrcelineBaseWmsClient):
"""
API client for RIO IFDM interpolated IRCEL - CELINE open data
RIO IFDM is more fine-grained for interpolation than RIO but only allows one feature to be request at a time, which
may be slower
"""
_base_url = _rio_ifdm_wms_base_url
async def get_data(self,
features: List[RioIfdmFeature],
position: Tuple[float, float]
) -> Dict[RioIfdmFeature, FeatureValue]:
"""
Get interpolated concentrations for the given features at the given position.
:param features: pollutants to get the forecasts for
:param position: (lat, long)
:return: dict where key is RioIfdmFeature and value is a FeatureValue
"""
result = dict()
lat, lon = position
base_querystring = (self._default_querystring |
{"bbox": f"{lon},{lat},{lon + self._epsilon},{lat + self._epsilon}"})
tasks = [asyncio.create_task(self._get_single_feature(base_querystring, feature)) for feature in features]
results = await asyncio.gather(*tasks)
for r in results:
result |= r
return result
async def _get_single_feature(self, base_querystring: dict, feature: RioIfdmFeature) -> dict:
result = dict()
querystring = base_querystring | {"layers": f"{feature}", "query_layers": f"{feature}"}
try:
r: ClientResponse = await self._api_wrapper(self._base_url, querystring)
r: dict = await r.json()
result[feature] = FeatureValue(
value=r.get('features', [{}])[0].get('properties', {}).get('GRAY_INDEX'),
timestamp=datetime.fromisoformat(r.get('timeStamp')) if 'timeStamp' in r else None)
except (IrcelineApiError, ClientResponseError, IndexError):
result[feature] = FeatureValue(value=None, timestamp=None)
return result

View file

@ -1,4 +1,3 @@
from collections import OrderedDict
from typing import Tuple from typing import Tuple
from pyproj import Transformer from pyproj import Transformer
@ -6,31 +5,6 @@ from pyproj import Transformer
_project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=False) _project_transform = Transformer.from_crs('EPSG:4326', 'EPSG:31370', always_xy=False)
class SizedDict(OrderedDict):
"""Dictionary with a maximum size. When more items are added, the least recently accessed element is evicted"""
def __init__(self, size: int):
super().__init__()
self._size = size
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.move_to_end(key)
if len(self) > self._size:
self.popitem(False)
def __getitem__(self, key):
self.move_to_end(key)
return super().__getitem__(key)
def get(self, __key, __default=None):
self.move_to_end(__key)
return super().get(__key, __default)
def update(self, __m, **kwargs):
raise NotImplementedError()
def epsg_transform(position: Tuple[float, float]) -> Tuple[int, int]: def epsg_transform(position: Tuple[float, float]) -> Tuple[int, int]:
""" """
Convert 'EPSG:4326' coordinates to 'EPSG:31370' coordinates Convert 'EPSG:4326' coordinates to 'EPSG:31370' coordinates
@ -39,15 +13,3 @@ def epsg_transform(position: Tuple[float, float]) -> Tuple[int, int]:
""" """
result = _project_transform.transform(position[0], position[1]) result = _project_transform.transform(position[0], position[1])
return round(result[0]), round(result[1]) return round(result[0]), round(result[1])
def round_coordinates(x: float, y: float, step=.05) -> Tuple[float, float]:
"""
Round the coordinate to the precision given by step
:param x: latitude
:param y: longitude
:param step: precision of the rounding
:return: x and y round to the closest step increment
"""
n = 1 / step
return round(x * n) / n, round(y * n) / n

View file

@ -0,0 +1,17 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "",
"geometry": null,
"properties": {
"GRAY_INDEX": 84.33950805664062
}
}
],
"totalFeatures": "unknown",
"numberReturned": 1,
"timeStamp": "2024-06-30T15:43:07.222Z",
"crs": null
}

694
tests/fixtures/rio_ifdm_capabilities.xml vendored Normal file
View file

@ -0,0 +1,694 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE WMT_MS_Capabilities SYSTEM "https://geobelair.irceline.be/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd">
<WMT_MS_Capabilities version="1.1.1" updateSequence="4129">
<Service>
<Name>OGC:WMS</Name>
<Title>IRCEL - CELINE - Web Map Service</Title>
<Abstract>A compliant implementation of WMS plus most of the SLD extension (dynamic styling). Can also generate
PDF, SVG, KML, GeoRSS
</Abstract>
<KeywordList>
<Keyword>WFS</Keyword>
<Keyword>WMS</Keyword>
<Keyword>GEOSERVER</Keyword>
</KeywordList>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="http://geo.irceline.be/rioifdm/wms"/>
<ContactInformation>
<ContactPersonPrimary>
<ContactPerson>IRCEL - CELINE</ContactPerson>
<ContactOrganization>Belgian Interregional Environment Agency</ContactOrganization>
</ContactPersonPrimary>
<ContactPosition/>
<ContactAddress>
<AddressType/>
<Address>Gaucheretstraat 92-94 Rue Gaucheret</Address>
<City>Brussels</City>
<StateOrProvince/>
<PostCode>1030</PostCode>
<Country>Belgium</Country>
</ContactAddress>
<ContactVoiceTelephone>+(32)(0)2 227 57 01</ContactVoiceTelephone>
<ContactFacsimileTelephone/>
<ContactElectronicMailAddress>info@irceline.be</ContactElectronicMailAddress>
</ContactInformation>
<Fees>NONE</Fees>
<AccessConstraints>NONE</AccessConstraints>
</Service>
<Capability>
<Request>
<GetCapabilities>
<Format>application/vnd.ogc.wms_xml</Format>
<Format>text/xml</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
<Post>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Post>
</HTTP>
</DCPType>
</GetCapabilities>
<GetMap>
<Format>image/png</Format>
<Format>application/atom xml</Format>
<Format>application/atom+xml</Format>
<Format>application/json;type=utfgrid</Format>
<Format>application/openlayers</Format>
<Format>application/openlayers2</Format>
<Format>application/openlayers3</Format>
<Format>application/pdf</Format>
<Format>application/rss xml</Format>
<Format>application/rss+xml</Format>
<Format>application/vnd.google-earth.kml</Format>
<Format>application/vnd.google-earth.kml xml</Format>
<Format>application/vnd.google-earth.kml+xml</Format>
<Format>application/vnd.google-earth.kml+xml;mode=networklink</Format>
<Format>application/vnd.google-earth.kmz</Format>
<Format>application/vnd.google-earth.kmz xml</Format>
<Format>application/vnd.google-earth.kmz+xml</Format>
<Format>application/vnd.google-earth.kmz;mode=networklink</Format>
<Format>atom</Format>
<Format>image/geotiff</Format>
<Format>image/geotiff8</Format>
<Format>image/gif</Format>
<Format>image/gif;subtype=animated</Format>
<Format>image/jpeg</Format>
<Format>image/png8</Format>
<Format>image/png; mode=8bit</Format>
<Format>image/svg</Format>
<Format>image/svg xml</Format>
<Format>image/svg+xml</Format>
<Format>image/tiff</Format>
<Format>image/tiff8</Format>
<Format>image/vnd.jpeg-png</Format>
<Format>image/vnd.jpeg-png8</Format>
<Format>kml</Format>
<Format>kmz</Format>
<Format>openlayers</Format>
<Format>rss</Format>
<Format>text/html; subtype=openlayers</Format>
<Format>text/html; subtype=openlayers2</Format>
<Format>text/html; subtype=openlayers3</Format>
<Format>utfgrid</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
</HTTP>
</DCPType>
</GetMap>
<GetFeatureInfo>
<Format>text/plain</Format>
<Format>application/vnd.ogc.gml</Format>
<Format>text/xml</Format>
<Format>application/vnd.ogc.gml/3.1.1</Format>
<Format>text/xml; subtype=gml/3.1.1</Format>
<Format>text/html</Format>
<Format>application/json</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
<Post>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Post>
</HTTP>
</DCPType>
</GetFeatureInfo>
<DescribeLayer>
<Format>application/vnd.ogc.wms_xml</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
</HTTP>
</DCPType>
</DescribeLayer>
<GetLegendGraphic>
<Format>image/png</Format>
<Format>image/jpeg</Format>
<Format>application/json</Format>
<Format>image/gif</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
</HTTP>
</DCPType>
</GetLegendGraphic>
<GetStyles>
<Format>application/vnd.ogc.sld+xml</Format>
<DCPType>
<HTTP>
<Get>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?SERVICE=WMS&amp;"/>
</Get>
</HTTP>
</DCPType>
</GetStyles>
</Request>
<Exception>
<Format>application/vnd.ogc.se_xml</Format>
<Format>application/vnd.ogc.se_inimage</Format>
<Format>application/vnd.ogc.se_blank</Format>
<Format>application/json</Format>
</Exception>
<UserDefinedSymbolization SupportSLD="1" UserLayer="1" UserStyle="1" RemoteWFS="1"/>
<Layer>
<Title>IRCEL - CELINE - Web Map Service</Title>
<Abstract>A compliant implementation of WMS plus most of the SLD extension (dynamic styling). Can also
generate PDF, SVG, KML, GeoRSS
</Abstract>
<!--Limited list of EPSG projections:-->
<SRS>EPSG:3857</SRS>
<SRS>EPSG:4258</SRS>
<SRS>EPSG:4326</SRS>
<SRS>EPSG:31370</SRS>
<SRS>EPSG:900913</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<AuthorityURL name="IRCEL - CELINE">
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://geo.irceline.be"/>
</AuthorityURL>
<Identifier authority="IRCEL - CELINE">http://geo.irceline.be</Identifier>
<Layer queryable="1" opaque="0">
<Name>belaqi</Name>
<Title>belaqi</Title>
<Abstract/>
<KeywordList>
<Keyword>belaqi</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>belaqi_raster_discrete_belair</Name>
<Title>AQ index for raster (discrete colour schale)</Title>
<Abstract>BelAQI index colour scale. Classic concentration color progression.</Abstract>
<LegendURL width="36" height="250">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=belaqi"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>belaqi_dm1</Name>
<Title>belaqi_dm1</Title>
<Abstract/>
<KeywordList>
<Keyword>belaqi_dmean_dm1</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>belaqi_raster_discrete_belair</Name>
<Title>AQ index for raster (discrete colour schale)</Title>
<Abstract>BelAQI index colour scale. Classic concentration color progression.</Abstract>
<LegendURL width="36" height="250">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=belaqi_dm1"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>belaqi_dm2</Name>
<Title>belaqi_dm2</Title>
<Abstract/>
<KeywordList>
<Keyword>belaqi_dmean_dm2</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>belaqi_raster_discrete_belair</Name>
<Title>AQ index for raster (discrete colour schale)</Title>
<Abstract>BelAQI index colour scale. Classic concentration color progression.</Abstract>
<LegendURL width="36" height="250">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=belaqi_dm2"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>belaqi_dm3</Name>
<Title>belaqi_dm3</Title>
<Abstract/>
<KeywordList>
<Keyword>belaqi_dmean_dm3</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>belaqi_raster_discrete_belair</Name>
<Title>AQ index for raster (discrete colour schale)</Title>
<Abstract>BelAQI index colour scale. Classic concentration color progression.</Abstract>
<LegendURL width="36" height="250">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=belaqi_dm3"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>no2_dm1</Name>
<Title>no2_dm1</Title>
<Abstract/>
<KeywordList>
<Keyword>no2_dmean_dm1</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>no2_dmean_raster_discrete_belair</Name>
<Title>Nitrogen dioxide (NO2) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for Nitrogen dioxide (NO2).
</Abstract>
<LegendURL width="80" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=no2_dm1"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>no2_dm2</Name>
<Title>no2_dm2</Title>
<Abstract/>
<KeywordList>
<Keyword>no2_dmean_dm2</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>no2_dmean_raster_discrete_belair</Name>
<Title>Nitrogen dioxide (NO2) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for Nitrogen dioxide (NO2).
</Abstract>
<LegendURL width="80" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=no2_dm2"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>no2_dm3</Name>
<Title>no2_dm3</Title>
<Abstract/>
<KeywordList>
<Keyword>no2_dmean_dm3</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>no2_dmean_raster_discrete_belair</Name>
<Title>Nitrogen dioxide (NO2) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for Nitrogen dioxide (NO2).
</Abstract>
<LegendURL width="80" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=no2_dm3"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>no2_hmean</Name>
<Title>no2_hmean</Title>
<Abstract/>
<KeywordList>
<Keyword>no2_hmean</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>no2_hmean_raster_discrete_belair</Name>
<Title>Nitrogen dioxide (NO2) hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Hourly mean concentration classes for Nitrogen dioxide (NO2).
</Abstract>
<LegendURL width="80" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=no2_hmean"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>o3_dm1</Name>
<Title>o3_dm1</Title>
<Abstract/>
<KeywordList>
<Keyword>o3_max8hmean_dm1</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>o3_max8hmean_raster_discrete_belair</Name>
<Title>Ozone (O3) daily max 8-hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily max 8-hourly mean concentration classes for Ozone (O3).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=o3_dm1"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>o3_dm2</Name>
<Title>o3_dm2</Title>
<Abstract/>
<KeywordList>
<Keyword>o3_max8hmean_dm2</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>o3_max8hmean_raster_discrete_belair</Name>
<Title>Ozone (O3) daily max 8-hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily max 8-hourly mean concentration classes for Ozone (O3).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=o3_dm2"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>o3_dm3</Name>
<Title>o3_dm3</Title>
<Abstract/>
<KeywordList>
<Keyword>o3_max8hmean_dm3</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>o3_max8hmean_raster_discrete_belair</Name>
<Title>Ozone (O3) daily max 8-hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily max 8-hourly mean concentration classes for Ozone (O3).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=o3_dm3"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>o3_hmean</Name>
<Title>o3_hmean</Title>
<Abstract/>
<KeywordList>
<Keyword>o3_hmean</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>o3_hmean_raster_discrete_belair</Name>
<Title>Ozone (O3) hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Annual mean concentration classes for Ozone (O3).</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=o3_hmean"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm10_dm1</Name>
<Title>pm10_dm1</Title>
<Abstract/>
<KeywordList>
<Keyword>pm10_dmean_dm1</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm10_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM10) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM10).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm10_dm1"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm10_dm2</Name>
<Title>pm10_dm2</Title>
<Abstract/>
<KeywordList>
<Keyword>pm10_dmean_dm2</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm10_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM10) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM10).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm10_dm2"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm10_dm3</Name>
<Title>pm10_dm3</Title>
<Abstract/>
<KeywordList>
<Keyword>pm10_dmean_dm3</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm10_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM10) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM10).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm10_dm3"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm10_hmean</Name>
<Title>pm10_hmean</Title>
<Abstract/>
<KeywordList>
<Keyword>pm10_hmean</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm10_hmean_raster_discrete_belair</Name>
<Title>particulate matter (PM10) hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Hourly mean concentration classes for particulate matter
(PM10).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm10_hmean"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm25_dm1</Name>
<Title>pm25_dm1</Title>
<Abstract/>
<KeywordList>
<Keyword>pm25_dmean_dm1</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm25_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM25) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM25).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm25_dm1"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm25_dm2</Name>
<Title>pm25_dm2</Title>
<Abstract/>
<KeywordList>
<Keyword>pm25_dmean_dm2</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm25_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM25) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM25).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm25_dm2"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm25_dm3</Name>
<Title>pm25_dm3</Title>
<Abstract/>
<KeywordList>
<Keyword>pm25_dmean_dm3</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm25_dmean_raster_discrete_belair</Name>
<Title>particulate matter (PM25) daily mean concentrations</Title>
<Abstract>BelAQI index colour scale. Daily mean concentration classes for particulate matter
(PM25).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm25_dm3"/>
</LegendURL>
</Style>
</Layer>
<Layer queryable="1" opaque="0">
<Name>pm25_hmean</Name>
<Title>pm25_hmean</Title>
<Abstract/>
<KeywordList>
<Keyword>pm25_hmean</Keyword>
<Keyword>WCS</Keyword>
<Keyword>GeoTIFF</Keyword>
</KeywordList>
<SRS>EPSG:31370</SRS>
<LatLonBoundingBox minx="2.4804079470216474" miny="49.461523892203324" maxx="6.494748595696256"
maxy="51.54189845090732"/>
<BoundingBox SRS="EPSG:31370" minx="18950.0" miny="18650.0" maxx="297550.0" maxy="248050.0"/>
<Style>
<Name>pm25_hmean_raster_discrete_belair</Name>
<Title>particulate matter (PM25) hourly mean concentrations</Title>
<Abstract>BelAQI index colour scale. Hourly mean concentration classes for particulate matter
(PM25).
</Abstract>
<LegendURL width="87" height="252">
<Format>image/png</Format>
<OnlineResource xmlns:xlink="http://www.w3.org/1999/xlink" xlink:type="simple"
xlink:href="https://geobelair.irceline.be/rioifdm/wms?request=GetLegendGraphic&amp;version=1.1.1&amp;format=image%2Fpng&amp;width=20&amp;height=20&amp;layer=pm25_hmean"/>
</LegendURL>
</Style>
</Layer>
</Layer>
</Capability>
</WMT_MS_Capabilities>

View file

@ -95,7 +95,7 @@ async def test_api_forecast():
client = IrcelineForecastClient(session) client = IrcelineForecastClient(session)
features = [ForecastFeature.NO2_DMEAN, ForecastFeature.O3_MAXHMEAN] features = [ForecastFeature.NO2_DMEAN, ForecastFeature.O3_MAXHMEAN]
_ = await client.get_data(features, pos) result = await client.get_data(features, pos)
base = {"service": "WMS", base = {"service": "WMS",
"version": "1.1.1", "version": "1.1.1",
@ -119,6 +119,9 @@ async def test_api_forecast():
session.request.assert_has_calls(calls, any_order=True) session.request.assert_has_calls(calls, any_order=True)
for k, v in result.items():
assert v['value'] == 10.853286743164062
def test_parse_capabilities_with_error(): def test_parse_capabilities_with_error():
result = IrcelineForecastClient._parse_capabilities("wow there no valid XML") result = IrcelineForecastClient._parse_capabilities("wow there no valid XML")

View file

@ -64,7 +64,7 @@ async def test_format_result_dmean():
def test_parse_capabilities(): def test_parse_capabilities():
data = get_api_data('capabilities.xml', plain=True) data = get_api_data('rio_capabilities.xml', plain=True)
result = IrcelineRioClient._parse_capabilities(data) result = IrcelineRioClient._parse_capabilities(data)
expected = {'rio:so2_anmean_be', 'rio:o3_hmean', 'rio:bc_anmean_vl', 'rio:o3_anmean_be', 'rio:pm10_hmean_vl', expected = {'rio:so2_anmean_be', 'rio:o3_hmean', 'rio:bc_anmean_vl', 'rio:o3_anmean_be', 'rio:pm10_hmean_vl',
@ -117,7 +117,7 @@ async def test_api_rio():
async def test_api_rio_get_capabilities(): async def test_api_rio_get_capabilities():
session = get_mock_session(text_file='capabilities.xml') session = get_mock_session(text_file='rio_capabilities.xml')
client = IrcelineRioClient(session) client = IrcelineRioClient(session)
_ = await client.get_capabilities() _ = await client.get_capabilities()

102
tests/test_api_rio_ifdm.py Normal file
View file

@ -0,0 +1,102 @@
from datetime import datetime
from unittest.mock import call
from freezegun import freeze_time
from src.open_irceline.api import _rio_ifdm_wms_base_url, _user_agent
from src.open_irceline.data import RioIfdmFeature, FeatureValue
from src.open_irceline.rio import IrcelineRioIfdmClient
from tests.conftest import get_api_data, get_mock_session
def test_parse_capabilities():
data = get_api_data('rio_ifdm_capabilities.xml', plain=True)
result = IrcelineRioIfdmClient._parse_capabilities(data)
expected = {'no2_dm3', 'belaqi_dm2', 'pm10_hmean', 'belaqi_dm1', 'pm25_dm3', 'pm25_dm2', 'pm10_dm1', 'o3_dm3',
'no2_dm1', 'pm10_dm3', 'pm25_dm1', 'belaqi', 'belaqi_dm3', 'pm10_dm2', 'o3_dm2', 'pm25_hmean', 'o3_dm1',
'o3_hmean', 'no2_dm2', 'no2_hmean'}
assert result == expected
for f in RioIfdmFeature:
assert f"{f.split(':')[1]}" in result
async def test_aget_capabilities():
session = get_mock_session(text_file='rio_ifdm_capabilities.xml')
client = IrcelineRioIfdmClient(session)
_ = await client.get_capabilities()
session.request.assert_called_once_with(
method='GET',
url=_rio_ifdm_wms_base_url,
params={"service": "WMS",
"version": "1.1.1",
"request": "GetCapabilities"},
headers={'User-Agent': _user_agent}
)
@freeze_time(datetime.fromisoformat("2024-06-30T13:00:21.520Z"))
async def test_api_forecast_error():
pos = (50.4657, 4.8647)
session = get_mock_session('forecast_wms_feature_info_invalid.json')
client = IrcelineRioIfdmClient(session)
features = [RioIfdmFeature.NO2_HMEAN, RioIfdmFeature.O3_HMEAN]
result = await client.get_data(features, pos)
for k, v in result.items():
assert v == FeatureValue(timestamp=datetime.fromisoformat("2024-06-30T13:00:21.520Z"), value=None)
async def test_api_forecast():
pos = (50.4657, 4.8647)
lat, lon = pos
session = get_mock_session('forecast_wms_feature_info.json')
client = IrcelineRioIfdmClient(session)
features = [RioIfdmFeature.NO2_HMEAN, RioIfdmFeature.O3_HMEAN]
result = await client.get_data(features, pos)
base = {"service": "WMS",
"version": "1.1.1",
"request": "GetFeatureInfo",
"info_format": "application/json",
"width": "1",
"height": "1",
"srs": "EPSG:4326",
"bbox": f"{lon},{lat},{lon + 0.00001},{lat + 0.00001}",
"X": "1",
"Y": "1"}
calls = [call(
method='GET',
url=_rio_ifdm_wms_base_url,
params=base | {"layers": f"{feature}",
"query_layers": f"{feature}"},
headers={'User-Agent': _user_agent},
)
for feature in features]
session.request.assert_has_calls(calls, any_order=True)
for k, v in result.items():
assert v['value'] == 10.853286743164062
async def test_api_forecast_no_field():
pos = (50.4657, 4.8647)
session = get_mock_session('forecast_wms_feature_info_no_field.json')
client = IrcelineRioIfdmClient(session)
features = [RioIfdmFeature.NO2_HMEAN, RioIfdmFeature.O3_HMEAN]
result = await client.get_data(features, pos)
for k, v in result.items():
assert v == FeatureValue(timestamp=None, value=None)

View file

@ -1,44 +1,4 @@
import pytest from src.open_irceline.utils import epsg_transform
from src.open_irceline.utils import SizedDict, round_coordinates, epsg_transform
def test_sized_dict():
s_dict = SizedDict(5)
assert len(s_dict) == 0
s_dict['a'] = 1
s_dict['b'] = 2
s_dict['c'] = 3
s_dict['d'] = 4
s_dict['e'] = 5
assert len(s_dict) == 5
s_dict['f'] = 6
assert 'a' not in s_dict
assert s_dict['f'] == 6
assert len(s_dict) == 5
s_dict['b'] = 42
s_dict['g'] = 7
assert s_dict.get('f') == 6
assert s_dict['g'] == 7
assert s_dict['b'] == 42
assert 'c' not in s_dict
assert len(s_dict) == 5
del s_dict['b']
assert len(s_dict) == 4
assert 'b' not in s_dict
with pytest.raises(NotImplementedError):
s_dict.update({'a': 1})
def test_round_coord():
x, y = round_coordinates(50.4657, 4.8647)
assert x == 50.45
assert y == 4.85
def test_epsg_transform(): def test_epsg_transform():