diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 3ab15ae9..a06ba605 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -35,7 +35,8 @@ }, "cStandard": "${default}", "cppStandard": "${default}", - "compilerPath": "${default}" + "compilerPath": "${default}", + "configurationProvider": "ms-vscode.cmake-tools" } ], "version": 4 diff --git a/Dockerfile b/Dockerfile index 1951bebe..c126937f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ USER root LABEL maintainer="adaguc@knmi.nl" # Version should be same as in Definitions.h -LABEL version="2.17.0" +LABEL version="2.18.0" # Try to update image packages RUN apt-get -q -y update \ diff --git a/NEWS.md b/NEWS.md index dec8353b..af45ef04 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +**Version 2.18.0 2024-02-14** +- Support live update layer, displays a GetMap image with the current time per second for the last hour. + **Version 2.17.0 2024-02-14** - Colors in GetMap images now matches exactly the colors configured in the legend and style in png32 mode - CSV reader is more flexible diff --git a/adagucserverEC/CDataSource.cpp b/adagucserverEC/CDataSource.cpp index 530b45b7..d34431b2 100644 --- a/adagucserverEC/CDataSource.cpp +++ b/adagucserverEC/CDataSource.cpp @@ -401,6 +401,8 @@ int CDataSource::setCFGLayer(CServerParams *_srvParams, CServerConfig::XMLE_Conf dLayerType = CConfigReaderLayerTypeUnknown; } else if (cfgLayer->attr.type.equals("baselayer")) { dLayerType = CConfigReaderLayerTypeBaseLayer; + } else if (cfgLayer->attr.type.equals("liveupdate")) { + dLayerType = CConfigReaderLayerTypeLiveUpdate; } else if (cfgLayer->attr.type.empty() == false) { if (strlen(cfgLayer->attr.type.c_str()) > 0) { dLayerType = CConfigReaderLayerTypeUnknown; diff --git a/adagucserverEC/CMakeLists.txt b/adagucserverEC/CMakeLists.txt index e2b66ac7..9f144f7a 100644 --- a/adagucserverEC/CMakeLists.txt +++ b/adagucserverEC/CMakeLists.txt @@ -154,6 +154,8 @@ add_library( CXMLSerializerInterface.h json.c json.h + LayerTypeLiveUpdate/LayerTypeLiveUpdate.cpp + LayerTypeLiveUpdate/LayerTypeLiveUpdate.h testadagucserver.cpp ) diff --git a/adagucserverEC/CRequest.cpp b/adagucserverEC/CRequest.cpp index 81097b1e..52c471af 100644 --- a/adagucserverEC/CRequest.cpp +++ b/adagucserverEC/CRequest.cpp @@ -37,6 +37,7 @@ #include "CSLD.h" #include "CHandleMetadata.h" #include "CCreateTiles.h" +#include "LayerTypeLiveUpdate/LayerTypeLiveUpdate.h" const char *CRequest::className = "CRequest"; int CRequest::CGI = 0; @@ -1786,13 +1787,17 @@ int CRequest::process_all_layers() { dataSources[j]->addStep("", NULL); // dataSources[j]->getCDFDims()->addDimension("none","0",0); } + if (dataSources[j]->dLayerType == CConfigReaderLayerTypeLiveUpdate) { + layerTypeLiveUpdateConfigureDimensionsInDataSource(dataSources[j]); + } } // Try to find BBOX automatically, when not provided. if (srvParam->requestType == REQUEST_WMS_GETMAP) { if (srvParam->dFound_BBOX == 0) { for (size_t d = 0; d < dataSources.size(); d++) { - if (dataSources[d]->dLayerType != CConfigReaderLayerTypeCascaded && dataSources[d]->dLayerType != CConfigReaderLayerTypeBaseLayer) { + if (dataSources[d]->dLayerType != CConfigReaderLayerTypeCascaded && dataSources[d]->dLayerType != CConfigReaderLayerTypeBaseLayer && + dataSources[d]->dLayerType != CConfigReaderLayerTypeLiveUpdate) { CImageWarper warper; CDataReader reader; status = reader.open(dataSources[d], CNETCDFREADER_MODE_OPEN_HEADER); @@ -2266,6 +2271,12 @@ int CRequest::process_all_layers() { CDBError("Returning from line %d", i); return 1; } + } else if (dataSources[j]->dLayerType == CConfigReaderLayerTypeLiveUpdate) { + // Render the current time in an image for testing purpose / frontend development + CDrawImage image; + layerTypeLiveUpdateRenderIntoDrawImage(&image, srvParam); + printf("%s%c%c\n", "Content-Type:image/png", 13, 10); + image.printImagePng8(true); } else { CDBError("Unknown layer type"); } diff --git a/adagucserverEC/CXMLGen.cpp b/adagucserverEC/CXMLGen.cpp index eec660b5..ea1bed57 100644 --- a/adagucserverEC/CXMLGen.cpp +++ b/adagucserverEC/CXMLGen.cpp @@ -29,6 +29,7 @@ #include #include "CXMLGen.h" #include "CDBFactory.h" +#include "LayerTypeLiveUpdate/LayerTypeLiveUpdate.h" // #define CXMLGEN_DEBUG const char *CFile::className = "CFile"; @@ -184,6 +185,13 @@ int CXMLGen::getDataSourceForLayer(WMSLayer *myWMSLayer) { } return 0; } + // This a liveupdate layer + if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeLiveUpdate) { +#ifdef CXMLGEN_DEBUG + CDBDebug("Live update layer"); +#endif + return layerTypeLiveUpdateConfigureWMSLayerForGetCapabilities(myWMSLayer); + } if (myWMSLayer->fileName.empty()) { CDBError("No file name specified for layer %s", myWMSLayer->dataSource->layerName.c_str()); return 1; @@ -252,6 +260,12 @@ int CXMLGen::getProjectionInformationForLayer(WMSLayer *myWMSLayer) { } } + if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeLiveUpdate) { + if (myWMSLayer->dataSource->cfgLayer->LatLonBox.size() == 0) { + return 0; + } + } + CGeoParams geo; CImageWarper warper; int status; @@ -718,6 +732,9 @@ int CXMLGen::getStylesForLayer(WMSLayer *myWMSLayer) { if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeCascaded) { return 0; } + if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeLiveUpdate) { + return 0; + } CT::PointerList *styleList = myWMSLayer->dataSource->getStyleListForDataSource(myWMSLayer->dataSource); @@ -1735,7 +1752,7 @@ int CXMLGen::OGCGetCapabilities(CServerParams *_srvParam, CT::string *XMLDocumen if (status != 0) myWMSLayer->hasError = 1; } - if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeCascaded) { + if (myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeCascaded || myWMSLayer->dataSource->dLayerType == CConfigReaderLayerTypeLiveUpdate) { myWMSLayer->isQuerable = 0; if (srvParam->serviceType == SERVICE_WCS) { myWMSLayer->hasError = true; @@ -1756,7 +1773,7 @@ int CXMLGen::OGCGetCapabilities(CServerParams *_srvParam, CT::string *XMLDocumen // Auto configure styles if (myWMSLayer->hasError == false) { if (myWMSLayer->dataSource->cfgLayer->Styles.size() == 0) { - if (myWMSLayer->dataSource->dLayerType != CConfigReaderLayerTypeCascaded) { + if (myWMSLayer->dataSource->dLayerType != CConfigReaderLayerTypeCascaded && myWMSLayer->dataSource->dLayerType != CConfigReaderLayerTypeLiveUpdate) { #ifdef CXMLGEN_DEBUG CDBDebug("cfgLayer->attr.type %d", myWMSLayer->dataSource->dLayerType); #endif diff --git a/adagucserverEC/Definitions.h b/adagucserverEC/Definitions.h index e49248f2..252d585b 100755 --- a/adagucserverEC/Definitions.h +++ b/adagucserverEC/Definitions.h @@ -28,7 +28,7 @@ #ifndef Definitions_H #define Definitions_H -#define ADAGUCSERVER_VERSION "2.17.0" // Please also update in the Dockerfile to the same version +#define ADAGUCSERVER_VERSION "2.18.0" // Please also update in the Dockerfile to the same version // CConfigReaderLayerType #define CConfigReaderLayerTypeUnknown 0 @@ -36,6 +36,7 @@ #define CConfigReaderLayerTypeStyled 2 #define CConfigReaderLayerTypeCascaded 3 #define CConfigReaderLayerTypeBaseLayer 4 +#define CConfigReaderLayerTypeLiveUpdate 6 // CRequest // Service options diff --git a/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.cpp b/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.cpp new file mode 100644 index 00000000..e46cd213 --- /dev/null +++ b/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.cpp @@ -0,0 +1,65 @@ +#include "LayerTypeLiveUpdate.h" + +int layerTypeLiveUpdateConfigureDimensionsInDataSource(CDataSource *dataSource) { + // This layer has no dimensions, but we need to add one timestep with data in order to make the next code work. + if (dataSource->requiredDims.size() < 1) { + + COGCDims *requiredDim = new COGCDims(); + requiredDim->isATimeDimension = true; + requiredDim->name = "time"; + requiredDim->netCDFDimName = "time"; + // The following values need to be filled in and are updated during XML GetCapabilities generation + requiredDim->uniqueValues.push_back("2020-01-01T00:00:00Z"); + requiredDim->uniqueValues.push_back("2020-01-02T00:00:00Z"); + requiredDim->value = "2020-01-02T00:00:00Z"; + dataSource->requiredDims.push_back(requiredDim); + } + dataSource->addStep("", NULL); + dataSource->getCDFDims()->addDimension("none", "0", 0); + return 0; +} + +int layerTypeLiveUpdateRenderIntoDrawImage(CDrawImage *image, CServerParams *srvParam) { + image->enableTransparency(true); + image->setTrueColor(true); + image->createImage(srvParam->Geo); + image->create685Palette(); + image->rectangle(0, 0, srvParam->Geo->dWidth, srvParam->Geo->dHeight, CColor(255, 255, 255, 0), CColor(255, 255, 255, 255)); + const char *fontFile = image->getFontLocation(); + CT::string timeValue = "No time dimension specified"; + if (srvParam->requestDims.size() == 1) { + timeValue = srvParam->requestDims[0]->value.c_str(); + } + int stepY = 100; + for (int y = 0; y < image->getHeight(); y = y + stepY) { + for (int x = 0; x < image->getWidth(); x = x + 300) { + image->drawText(x + (((y % (stepY * 2)) / stepY) * 150), y, fontFile, 15, 0.1, timeValue.c_str(), CColor(0, 0, 0, 255)); + } + } + return 0; +} + +int layerTypeLiveUpdateConfigureWMSLayerForGetCapabilities(WMSLayer *myWMSLayer) { + if (myWMSLayer->dataSource->cfgLayer->Title.size() != 0) { + myWMSLayer->title.copy(myWMSLayer->dataSource->cfgLayer->Title[0]->value.c_str()); + } else { + myWMSLayer->title.copy(myWMSLayer->dataSource->cfgLayer->Name[0]->value.c_str()); + } + CTime timeInstance; + timeInstance.init("seconds since 1970", "standard"); + double epochTime = timeInstance.getEpochTimeFromDateString(CTime::currentDateTime()); + // CTime::Date cdate = timeInstance.getDate(epochTime); + double startTimeOffset = timeInstance.quantizeTimeToISO8601(epochTime - 3600, "PT1S", "low"); + double stopTimeOffset = timeInstance.quantizeTimeToISO8601(epochTime, "PT1S", "low"); + CT::string startTime = timeInstance.dateToISOString(timeInstance.offsetToDate(startTimeOffset)); + CT::string stopTime = timeInstance.dateToISOString(timeInstance.offsetToDate(stopTimeOffset)); + CT::string resTime = "PT1S"; + WMSLayer::Dim *dim = new WMSLayer::Dim(); + myWMSLayer->dimList.push_back(dim); + dim->name.copy("time"); + dim->units.copy("ISO8601"); + dim->values.copy(startTime + "/" + stopTime + "/" + resTime); + dim->defaultValue.copy(stopTime.c_str()); + dim->hasMultipleValues = true; + return 0; +} \ No newline at end of file diff --git a/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.h b/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.h new file mode 100644 index 00000000..1578379f --- /dev/null +++ b/adagucserverEC/LayerTypeLiveUpdate/LayerTypeLiveUpdate.h @@ -0,0 +1,24 @@ +#include "CDataSource.h" +#include "CDrawImage.h" +#include "CServerParams.h" +#include "CServerParams.h" +#include "CXMLGen.h" + +#ifndef LAYERTYPELIVEUPDATE_H + +/** + * Configures a datasource with a fake time dimension + */ +int layerTypeLiveUpdateConfigureDimensionsInDataSource(CDataSource *dataSource); + +/** + * Renders time into an image the based on the time dimension in the GetMap request. + */ +int layerTypeLiveUpdateRenderIntoDrawImage(CDrawImage *image, CServerParams *srvParam); + +/** + * Configures an actual time range in a WMSLayer object. This is used for generating the Layer element in the WMS GetCapabilities file + */ +int layerTypeLiveUpdateConfigureWMSLayerForGetCapabilities(WMSLayer *myWMSLayer); + +#endif // !LAYERTYPELIVEUPDATE_H diff --git a/data/config/datasets/liveupdatewms.xml b/data/config/datasets/liveupdatewms.xml new file mode 100644 index 00000000..bc029ce0 --- /dev/null +++ b/data/config/datasets/liveupdatewms.xml @@ -0,0 +1,11 @@ + + + + + liveupdate + + + diff --git a/doc/configuration/2024-02-14-liveupdate-layer.png b/doc/configuration/2024-02-14-liveupdate-layer.png new file mode 100644 index 00000000..d43b9b58 Binary files /dev/null and b/doc/configuration/2024-02-14-liveupdate-layer.png differ diff --git a/doc/configuration/Layer.md b/doc/configuration/Layer.md index 515db4a0..9b329ca9 100644 --- a/doc/configuration/Layer.md +++ b/doc/configuration/Layer.md @@ -3,7 +3,7 @@ Layer (type,hidden) Back to [Configuration](./Configuration.md) -- type - The type of layer, can be either database,grid or cascaded +- type - The type of layer, can be either database,grid, liveupdate or cascaded - hidden - When set to true, the Layer is not advertised in the GetCapabilities document, but is accessible with GetMap requests. @@ -51,3 +51,23 @@ compose nice maps. ``` + +Liveupdate layer +--------------- + +This layer type displays a GetMap image with the current time per second for the last hour. + +```xml + + + + + liveupdate + + +``` + + diff --git a/doc/developing/scripts/ubuntu_22_install_dependencies.md b/doc/developing/scripts/ubuntu_22_install_dependencies.md index 433db0ba..44bf661e 100644 --- a/doc/developing/scripts/ubuntu_22_install_dependencies.md +++ b/doc/developing/scripts/ubuntu_22_install_dependencies.md @@ -7,5 +7,5 @@ sudo apt-get update -sudo apt-get install cmake python3-lxml postgresql libcurl4-openssl-dev libcairo2-dev libxml2-dev libgd-dev postgresql-server-dev-all postgresql-client libudunits2-dev udunits-bin g++ m4 netcdf-bin libnetcdf-dev libhdf5-dev libsqlite3-dev python3-netcdf4 python3-dev sqlite3 libsqlite3-dev libwebp-dev libproj-dev proj-bin libproj22 +sudo apt-get install cmake python3-lxml postgresql libcurl4-openssl-dev libcairo2-dev libxml2-dev libgd-dev postgresql-server-dev-all postgresql-client libudunits2-dev udunits-bin g++ m4 netcdf-bin libnetcdf-dev libhdf5-dev libsqlite3-dev python3-netcdf4 python3-dev sqlite3 libsqlite3-dev libwebp-dev libproj-dev proj-bin libproj22 libgdal-dev clang-format ```