Repository: qgis/QGIS
Pull request #61424: [WMS][Time dimension] Allow group node to expose time dimension
Author: login: troopa81
Labels: name: Feature
name: Server
description: Related to QGIS server
name: Needs Documentation
description: When merging a labeled PR, an issue will be created in the Doc repo.
name: Temporal
description: Temporal filtering or animation
name: ChangelogHarvested
description: This PR description has been harvested in the Changelog already.
Created: 2025-04-10T13:55:09Z | Merged: 2025-05-06T11:51:24Z

Allow layer tree group node to expose time dimension

A new checkbox allows to enable or disable the computation of this time dimension. If enabled, it would recursively generate the time dimension based upon its children time dimension. A group which time dimension computation is disabled would not propage the time dimension of its children to its parent.

![computetime](https://github.com/user-attachments/assets/c4f8e176-de60-4222-aaed-603449298f3a)

Add also support of date range in time dimension with respect to the OGC WMS and ISO8601 standard.

**Funded by Ifremer**



Diff:
diff --git a/python/PyQt6/core/auto_generated/layertree/qgslayertreegroup.sip.in b/python/PyQt6/core/auto_generated/layertree/qgslayertreegroup.sip.in
index ed23f0cb1895..f59d1f7e6a94 100644
--- a/python/PyQt6/core/auto_generated/layertree/qgslayertreegroup.sip.in
+++ b/python/PyQt6/core/auto_generated/layertree/qgslayertreegroup.sip.in
@@ -299,6 +299,34 @@ Returns QGIS Server Properties for the layer tree group
 %End
 
 
+    void setHasWmsTimeDimension( const bool hasWmsTimeDimension );
+%Docstring
+Sets whether the WMS time dimension should be computed for this group or
+not
+
+:param hasWmsTimeDimension: if ``True``, when a GetCapabilities request
+                            is sent, QGIS server would return a TIME
+                            dimension computed as an union of all time
+                            dimensions of its children recursively.
+                            Else, no TIME dimension will be returned.
+
+.. seealso:: :py:func:`hasWmsTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
+    bool hasWmsTimeDimension() const;
+%Docstring
+Returns whether the WMS time dimension should be computed for this group
+or not. if ``True``, when a GetCapabilities request is sent, QGIS server
+would return a TIME dimension computed as an union of all time
+dimensions of its children recursively. Else, no TIME dimension will be
+returned.
+
+.. seealso:: :py:func:`setHasWmsTimeDimension`
+
+.. versionadded:: 3.44
+%End
 
   protected slots:
 
@@ -316,6 +344,7 @@ Set check state of children - if mutually exclusive
 
 
 
+
   private:
 
     QgsLayerTreeGroup( const QgsLayerTreeGroup &other );
diff --git a/python/PyQt6/gui/auto_generated/qgsgroupwmsdatadialog.sip.in b/python/PyQt6/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
index 5349e4d9655b..d9b8ddf025d2 100644
--- a/python/PyQt6/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
+++ b/python/PyQt6/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
@@ -91,6 +91,34 @@ Sets group WMS abstract
    Use :py:func:`~QgsGroupWmsDataDialog.serverProperties`->:py:func:`~QgsGroupWmsDataDialog.setAbstract` instead.
 %End
 
+    void setHasTimeDimension( bool hasTimeDimension );
+%Docstring
+Sets whether the time dimension should be computed for this group or not
+
+:param hasTimeDimension: if ``True``, when a GetCapabilities request is
+                         sent, QGIS server would return a TIME dimension
+                         computed as an union of all time dimensions of
+                         its children recursively. Else, no TIME
+                         dimension will be returned.
+
+.. seealso:: :py:func:`hasTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
+    bool hasTimeDimension() const;
+%Docstring
+Returns whether the time dimension should be computed for this group or
+not. if ``True``, when a GetCapabilities request is sent, QGIS server
+would return a TIME dimension computed as an union of all time
+dimensions of its children recursively. Else, no TIME dimension will be
+returned.
+
+.. seealso:: :py:func:`setHasTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
     QgsMapLayerServerProperties *serverProperties();
 %Docstring
 Returns QGIS Server Properties for the layer tree group
diff --git a/python/core/auto_generated/layertree/qgslayertreegroup.sip.in b/python/core/auto_generated/layertree/qgslayertreegroup.sip.in
index ed23f0cb1895..f59d1f7e6a94 100644
--- a/python/core/auto_generated/layertree/qgslayertreegroup.sip.in
+++ b/python/core/auto_generated/layertree/qgslayertreegroup.sip.in
@@ -299,6 +299,34 @@ Returns QGIS Server Properties for the layer tree group
 %End
 
 
+    void setHasWmsTimeDimension( const bool hasWmsTimeDimension );
+%Docstring
+Sets whether the WMS time dimension should be computed for this group or
+not
+
+:param hasWmsTimeDimension: if ``True``, when a GetCapabilities request
+                            is sent, QGIS server would return a TIME
+                            dimension computed as an union of all time
+                            dimensions of its children recursively.
+                            Else, no TIME dimension will be returned.
+
+.. seealso:: :py:func:`hasWmsTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
+    bool hasWmsTimeDimension() const;
+%Docstring
+Returns whether the WMS time dimension should be computed for this group
+or not. if ``True``, when a GetCapabilities request is sent, QGIS server
+would return a TIME dimension computed as an union of all time
+dimensions of its children recursively. Else, no TIME dimension will be
+returned.
+
+.. seealso:: :py:func:`setHasWmsTimeDimension`
+
+.. versionadded:: 3.44
+%End
 
   protected slots:
 
@@ -316,6 +344,7 @@ Set check state of children - if mutually exclusive
 
 
 
+
   private:
 
     QgsLayerTreeGroup( const QgsLayerTreeGroup &other );
diff --git a/python/gui/auto_generated/qgsgroupwmsdatadialog.sip.in b/python/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
index 5349e4d9655b..d9b8ddf025d2 100644
--- a/python/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
+++ b/python/gui/auto_generated/qgsgroupwmsdatadialog.sip.in
@@ -91,6 +91,34 @@ Sets group WMS abstract
    Use :py:func:`~QgsGroupWmsDataDialog.serverProperties`->:py:func:`~QgsGroupWmsDataDialog.setAbstract` instead.
 %End
 
+    void setHasTimeDimension( bool hasTimeDimension );
+%Docstring
+Sets whether the time dimension should be computed for this group or not
+
+:param hasTimeDimension: if ``True``, when a GetCapabilities request is
+                         sent, QGIS server would return a TIME dimension
+                         computed as an union of all time dimensions of
+                         its children recursively. Else, no TIME
+                         dimension will be returned.
+
+.. seealso:: :py:func:`hasTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
+    bool hasTimeDimension() const;
+%Docstring
+Returns whether the time dimension should be computed for this group or
+not. if ``True``, when a GetCapabilities request is sent, QGIS server
+would return a TIME dimension computed as an union of all time
+dimensions of its children recursively. Else, no TIME dimension will be
+returned.
+
+.. seealso:: :py:func:`setHasTimeDimension`
+
+.. versionadded:: 3.44
+%End
+
     QgsMapLayerServerProperties *serverProperties();
 %Docstring
 Returns QGIS Server Properties for the layer tree group
diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp
index 122dd16d35f2..4101a2499a3c 100644
--- a/src/app/qgisapp.cpp
+++ b/src/app/qgisapp.cpp
@@ -12124,12 +12124,13 @@ void QgisApp::legendGroupSetWmsData()
     return;
 
   QgsGroupWmsDataDialog dlg( *currentGroup->serverProperties(), this );
-  if ( dlg.exec() )
+  dlg.setHasTimeDimension( currentGroup->hasWmsTimeDimension() );
+  if ( dlg.exec() && ( *dlg.serverProperties() != *currentGroup->serverProperties() || dlg.hasTimeDimension() != currentGroup->hasWmsTimeDimension() ) )
   {
-    if ( *dlg.serverProperties() != *currentGroup->serverProperties() )
-      QgsProject::instance()->setDirty( true );
+    QgsProject::instance()->setDirty( true );
 
     dlg.serverProperties()->copyTo( currentGroup->serverProperties() );
+    currentGroup->setHasWmsTimeDimension( dlg.hasTimeDimension() );
   }
 }
 
diff --git a/src/core/layertree/qgslayertreegroup.cpp b/src/core/layertree/qgslayertreegroup.cpp
index 7eaf2a3ade45..88b016a20d13 100644
--- a/src/core/layertree/qgslayertreegroup.cpp
+++ b/src/core/layertree/qgslayertreegroup.cpp
@@ -20,7 +20,6 @@
 #include "qgslayertreeutils.h"
 #include "qgsmaplayer.h"
 #include "qgsgrouplayer.h"
-#include "qgspainting.h"
 
 #include <QDomElement>
 #include <QStringList>
@@ -40,6 +39,7 @@ QgsLayerTreeGroup::QgsLayerTreeGroup( const QgsLayerTreeGroup &other )
   , mChangingChildVisibility( other.mChangingChildVisibility )
   , mMutuallyExclusive( other.mMutuallyExclusive )
   , mMutuallyExclusiveChildIndex( other.mMutuallyExclusiveChildIndex )
+  , mWmsHasTimeDimension( other.mWmsHasTimeDimension )
   , mGroupLayer( other.mGroupLayer )
   , mServerProperties( std::make_unique<QgsMapLayerServerProperties>() )
 {
@@ -370,6 +370,8 @@ QgsLayerTreeGroup *QgsLayerTreeGroup::readXml( const QDomElement &element, const
 
   groupNode->setIsMutuallyExclusive( isMutuallyExclusive, mutuallyExclusiveChildIndex );
 
+  groupNode->mWmsHasTimeDimension = element.attribute( QStringLiteral( "wms-has-time-dimension" ), QStringLiteral( "0" ) ) == QLatin1String( "1" );
+
   groupNode->mGroupLayer = QgsMapLayerRef( element.attribute( QStringLiteral( "groupLayer" ) ) );
 
   readLegacyServerProperties( groupNode );
@@ -426,6 +428,12 @@ void QgsLayerTreeGroup::writeXml( QDomElement &parentElement, const QgsReadWrite
     elem.setAttribute( QStringLiteral( "mutually-exclusive" ), QStringLiteral( "1" ) );
     elem.setAttribute( QStringLiteral( "mutually-exclusive-child" ), mMutuallyExclusiveChildIndex );
   }
+
+  if ( mWmsHasTimeDimension )
+  {
+    elem.setAttribute( QStringLiteral( "wms-has-time-dimension" ), QStringLiteral( "1" ) );
+  }
+
   elem.setAttribute( QStringLiteral( "groupLayer" ), mGroupLayer.layerId );
 
   writeCommonXml( elem );
@@ -686,3 +694,13 @@ const QgsMapLayerServerProperties *QgsLayerTreeGroup::serverProperties() const
 {
   return mServerProperties.get();
 }
+
+void QgsLayerTreeGroup::setHasWmsTimeDimension( const bool hasWmsTimeDimension )
+{
+  mWmsHasTimeDimension = hasWmsTimeDimension;
+}
+
+bool QgsLayerTreeGroup::hasWmsTimeDimension() const
+{
+  return mWmsHasTimeDimension;
+}
diff --git a/src/core/layertree/qgslayertreegroup.h b/src/core/layertree/qgslayertreegroup.h
index c3a0c6c87160..fad37fe9b7d3 100644
--- a/src/core/layertree/qgslayertreegroup.h
+++ b/src/core/layertree/qgslayertreegroup.h
@@ -287,6 +287,27 @@ class CORE_EXPORT QgsLayerTreeGroup : public QgsLayerTreeNode
      */
     const QgsMapLayerServerProperties *serverProperties() const SIP_SKIP;
 
+    /**
+     * Sets whether the WMS time dimension should be computed for this group or not
+     * \param hasWmsTimeDimension if TRUE, when a GetCapabilities request is sent,
+     * QGIS server would return a TIME dimension computed as an union of all time
+     * dimensions of its children recursively. Else, no TIME dimension will be returned.
+     *
+     * \see hasWmsTimeDimension()
+     * \since QGIS 3.44
+     */
+    void setHasWmsTimeDimension( const bool hasWmsTimeDimension );
+
+    /**
+     * Returns whether the WMS time dimension should be computed for this group or not.
+     * if TRUE, when a GetCapabilities request is sent, QGIS server would return a TIME
+     * dimension computed as an union of all time dimensions of its children recursively.
+     * Else, no TIME dimension will be returned.
+     *
+     * \see setHasWmsTimeDimension()
+     * \since QGIS 3.44
+     */
+    bool hasWmsTimeDimension() const;
 
   protected slots:
 
@@ -312,6 +333,8 @@ class CORE_EXPORT QgsLayerTreeGroup : public QgsLayerTreeNode
      */
     int mMutuallyExclusiveChildIndex = -1;
 
+    bool mWmsHasTimeDimension = false;
+
     //! Sets parent to NULLPTR and disconnects all external and forwarded signals
     virtual void makeOrphan() override SIP_SKIP;
 
diff --git a/src/gui/qgsgroupwmsdatadialog.cpp b/src/gui/qgsgroupwmsdatadialog.cpp
index 3741b1e93210..8b296754ed4f 100644
--- a/src/gui/qgsgroupwmsdatadialog.cpp
+++ b/src/gui/qgsgroupwmsdatadialog.cpp
@@ -87,3 +87,13 @@ void QgsGroupWmsDataDialog::accept()
   mMapLayerServerPropertiesWidget->save();
   QDialog::accept();
 }
+
+bool QgsGroupWmsDataDialog::hasTimeDimension() const
+{
+  return mComputeTimeDimension->checkState() == Qt::Checked;
+}
+
+void QgsGroupWmsDataDialog::setHasTimeDimension( bool hasTimeDimension )
+{
+  mComputeTimeDimension->setCheckState( hasTimeDimension ? Qt::Checked : Qt::Unchecked );
+}
diff --git a/src/gui/qgsgroupwmsdatadialog.h b/src/gui/qgsgroupwmsdatadialog.h
index 0aa0c7debbe9..8f1f0261d86b 100644
--- a/src/gui/qgsgroupwmsdatadialog.h
+++ b/src/gui/qgsgroupwmsdatadialog.h
@@ -90,6 +90,28 @@ class GUI_EXPORT QgsGroupWmsDataDialog : public QDialog, private Ui::QgsGroupWMS
      */
     Q_DECL_DEPRECATED void setGroupAbstract( const QString &abstract ) SIP_DEPRECATED;
 
+    /**
+     * Sets whether the time dimension should be computed for this group or not
+     * \param hasTimeDimension if TRUE, when a GetCapabilities request is sent,
+     * QGIS server would return a TIME dimension computed as an union of all time
+     * dimensions of its children recursively. Else, no TIME dimension will be returned.
+     *
+     * \see hasTimeDimension()
+     * \since QGIS 3.44
+     */
+    void setHasTimeDimension( bool hasTimeDimension );
+
+    /**
+     * Returns whether the time dimension should be computed for this group or not.
+     * if TRUE, when a GetCapabilities request is sent, QGIS server would return a TIME
+     * dimension computed as an union of all time dimensions of its children recursively.
+     * Else, no TIME dimension will be returned.
+     *
+     * \see setHasTimeDimension()
+     * \since QGIS 3.44
+     */
+    bool hasTimeDimension() const;
+
     /**
      * Returns QGIS Server Properties for the layer tree group
      * \since QGIS 3.44
diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp
index b4067e345b7f..289e069d533e 100644
--- a/src/server/services/wms/qgswmsgetcapabilities.cpp
+++ b/src/server/services/wms/qgswmsgetcapabilities.cpp
@@ -18,6 +18,7 @@
  *   (at your option) any later version.                                   *
  *                                                                         *
  ***************************************************************************/
+
 #include "qgswmsutils.h"
 #include "qgswmsgetcapabilities.h"
 #include "qgsserverprojectutils.h"
@@ -58,7 +59,7 @@ namespace QgsWms
 
     void appendLayerStyles( QDomDocument &doc, QDomElement &layerElem, const QgsWmsLayerInfos &layerInfos, const QgsProject *project, const QgsWmsRequest &request, const QgsServerSettings *settings );
 
-    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings );
+    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings, QList<QgsDateTimeRange> &parentDateRanges );
 
     void addKeywordListElement( const QgsProject *project, QDomDocument &doc, QDomElement &parent );
   } // namespace
@@ -752,7 +753,7 @@ namespace QgsWms
     return wfsLayersElem;
   }
 
-  void handleLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings )
+  void handleLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings, QList<QgsDateTimeRange> &parentDateRanges )
   {
     const auto layerIds = layerTreeGroup->findLayerIds();
 
@@ -768,7 +769,7 @@ namespace QgsWms
     appendLayerWgs84BoundingRect( doc, parentLayer, wgs84BoundingRect );
     appendLayerCrsExtents( doc, parentLayer, crsExtents );
 
-    appendLayersFromTreeGroup( doc, parentLayer, serverIface, project, request, layerTreeGroup, wmsLayerInfos, projectSettings );
+    appendLayersFromTreeGroup( doc, parentLayer, serverIface, project, request, layerTreeGroup, wmsLayerInfos, projectSettings, parentDateRanges );
   }
 
   QDomElement getLayersAndStylesCapabilitiesElement( QDomDocument &doc, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, bool projectSettings )
@@ -881,11 +882,13 @@ namespace QgsWms
       appendLayerWgs84BoundingRect( doc, layerParentElem, wmsWgs84BoundingRect );
       appendLayerCrsExtents( doc, layerParentElem, wmsCrsExtents );
 
-      appendLayersFromTreeGroup( doc, layerParentElem, serverIface, project, request, projectLayerTreeRoot, wmsLayerInfos, projectSettings );
+      QList<QgsDateTimeRange> parentDateRanges;
+      appendLayersFromTreeGroup( doc, layerParentElem, serverIface, project, request, projectLayerTreeRoot, wmsLayerInfos, projectSettings, parentDateRanges );
     }
     else
     {
-      handleLayersFromTreeGroup( doc, layerParentElem, serverIface, project, request, projectLayerTreeRoot, wmsLayerInfos, projectSettings );
+      QList<QgsDateTimeRange> parentDateRanges;
+      handleLayersFromTreeGroup( doc, layerParentElem, serverIface, project, request, projectLayerTreeRoot, wmsLayerInfos, projectSettings, parentDateRanges );
     }
 
     return layerParentElem;
@@ -1082,7 +1085,42 @@ namespace QgsWms
       return styleElem;
     }
 
-    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings )
+    //! Return TRUE if date only have been written, FALSE if there is datetime
+    bool writeTimeDimensionNode( QDomDocument &doc, QDomElement &layerElem, const QList<QgsDateTimeRange> &dateRanges )
+    {
+      // Apparently, for vectors allTemporalRanges is always empty :/
+      // there is no way to know the type of range or the individual instants
+
+      // we write a TIME dimension even if dateRanges is empty. Not sure this is appropriate but
+      // it was like that from the beginning so better keep it that way to avoid regression on client side
+
+      const bool hasDateTime = std::any_of( dateRanges.constBegin(), dateRanges.constEnd(), []( const QgsDateTimeRange &r ) { return r.begin().time() != QTime( 0, 0 )
+                                                                                                                                     || ( !r.isInstant() && r.end().time() != QTime( 0, 0 ) ); } );
+
+      const QString dateFormat = hasDateTime ? QStringLiteral( "yyyy-MM-ddTHH:mm:ss" ) : QStringLiteral( "yyyy-MM-dd" );
+
+      QStringList strValues;
+      for ( const QgsDateTimeRange &range : dateRanges )
+      {
+        // Standard ISO8601 doesn't support range with no defined begin or end
+        if ( range.begin().isValid() && range.end().isValid() )
+        {
+          strValues << ( range.isInstant() ? range.begin().toString( dateFormat ) : QStringLiteral( "%1/%2" ).arg( range.begin().toString( dateFormat ) ).arg( range.end().toString( dateFormat ) ) );
+        }
+      }
+
+      QDomElement dimElem = doc.createElement( QStringLiteral( "Dimension" ) );
+      dimElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "TIME" ) );
+      dimElem.setAttribute( QStringLiteral( "units" ), QStringLiteral( "ISO8601" ) );
+      QDomText dimValuesText = doc.createTextNode( strValues.join( QChar( ',' ) ) );
+      dimElem.appendChild( dimValuesText );
+
+      layerElem.appendChild( dimElem );
+
+      return !hasDateTime;
+    }
+
+    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings, QList<QgsDateTimeRange> &parentDateRanges )
     {
       const QString version = request.wmsParameters().version();
 
@@ -1117,7 +1155,7 @@ namespace QgsWms
             layerElem.setAttribute( QStringLiteral( "mutuallyExclusive" ), treeGroupChild->isMutuallyExclusive() );
           }
 
-          QString shortName = treeGroupChild->serverProperties()->shortName();
+          const QString shortName = treeGroupChild->serverProperties()->shortName();
 
           if ( !skipNameForGroup )
           {
@@ -1149,7 +1187,14 @@ namespace QgsWms
             layerElem.appendChild( treeNameElem );
           }
 
-          handleLayersFromTreeGroup( doc, layerElem, serverIface, project, request, treeGroupChild, wmsLayerInfos, projectSettings );
+          QList<QgsDateTimeRange> childrenDateRanges;
+          handleLayersFromTreeGroup( doc, layerElem, serverIface, project, request, treeGroupChild, wmsLayerInfos, projectSettings, childrenDateRanges );
+
+          if ( treeGroupChild->hasWmsTimeDimension() )
+          {
+            writeTimeDimensionNode( doc, layerElem, childrenDateRanges );
+            parentDateRanges.append( childrenDateRanges );
+          }
 
           // Check if child layer elements have been added
           if ( layerElem.elementsByTagName( QStringLiteral( "Layer" ) ).length() == 0 )
@@ -1313,61 +1358,13 @@ namespace QgsWms
                && l->temporalProperties()
                && l->temporalProperties()->isActive() )
           {
-            QDomElement dimElem = doc.createElement( QStringLiteral( "Dimension" ) );
-            dimElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "TIME" ) );
-            dimElem.setAttribute( QStringLiteral( "units" ), QStringLiteral( "ISO8601" ) );
-
             // TODO: set "default" (reference value)
 
             // Add all values
             const QList<QgsDateTimeRange> allRanges { l->temporalProperties()->allTemporalRanges( l ) };
+            const bool isDateList = writeTimeDimensionNode( doc, layerElem, allRanges );
 
-            // Apparently, for vectors allTemporalRanges is always empty :/
-            // there is no way to know the type of range or the individual instants
-
-            bool isDateList { true };
-            bool isInstantList { true };
-
-            QList<QDateTime> values;
-            for ( const auto &r : std::as_const( allRanges ) )
-            {
-              if ( r.isInstant() )
-              {
-                if ( r.begin().time() != QTime( 0, 0, 0, 0 ) )
-                {
-                  isDateList = false;
-                }
-                values.append( r.begin() );
-              }
-              else
-              {
-                isInstantList = false;
-                break;
-              }
-            }
-
-            // Only list individual values for list of instants,
-            // otherwise only the extent will be shown
-            if ( isInstantList )
-            {
-              // values list
-              QStringList strValues;
-              for ( const auto &v : values )
-              {
-                if ( isDateList )
-                {
-                  strValues << v.date().toString( Qt::DateFormat::ISODate );
-                }
-                else
-                {
-                  strValues << v.toString( Qt::DateFormat::ISODate );
-                }
-              }
-              QDomText dimValuesText = doc.createTextNode( strValues.join( QChar( ',' ) ) );
-              dimElem.appendChild( dimValuesText );
-            }
-
-            layerElem.appendChild( dimElem );
+            parentDateRanges.append( allRanges );
 
             QDomElement timeExtentElem = doc.createElement( QStringLiteral( "Extent" ) );
             timeExtentElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "TIME" ) );
diff --git a/src/ui/qgsgroupwmsdatadialogbase.ui b/src/ui/qgsgroupwmsdatadialogbase.ui
index f0d7870b5efb..bc2d07e21472 100644
--- a/src/ui/qgsgroupwmsdatadialogbase.ui
+++ b/src/ui/qgsgroupwmsdatadialogbase.ui
@@ -10,13 +10,23 @@
   </property>
   <property name="windowIcon">
    <iconset>
-    <normaloff/>
+     <normaloff/>
    </iconset>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QgsMapLayerServerPropertiesWidget" name="mMapLayerServerPropertiesWidget" native="true"/>
    </item>
+   <item>
+    <widget class="QCheckBox" name="mComputeTimeDimension">
+     <property name="toolTip">
+      <string>When a GetCapabilities request is sent, QGIS server will return a TIME dimension computed as an union of all time dimension of its children recursively</string>
+     </property>
+     <property name="text">
+      <string>Compute TIME dimension from children</string>
+     </property>
+    </widget>
+   </item>
    <item>
     <spacer name="verticalSpacer">
      <property name="orientation">
diff --git a/tests/src/python/test_qgsserver_wms_getmap.py b/tests/src/python/test_qgsserver_wms_getmap.py
index 7d0c5d10441f..93d0fe2873d0 100644
--- a/tests/src/python/test_qgsserver_wms_getmap.py
+++ b/tests/src/python/test_qgsserver_wms_getmap.py
@@ -27,13 +27,16 @@
 
 from lxml import etree as et
 from qgis.core import (
+    Qgis,
     QgsFeature,
     QgsGeometry,
     QgsProject,
     QgsVectorLayer,
     QgsVectorLayerTemporalProperties,
+    QgsDateTimeRange,
     QgsTextFormat,
     QgsPalLayerSettings,
+    QgsRasterLayer,
     QgsVectorLayerSimpleLabeling,
     QgsFontUtils,
 )
@@ -43,7 +46,7 @@
     QgsBufferServerResponse,
 )
 
-from qgis.PyQt.QtCore import QDate, QDateTime, QTime
+from qgis.PyQt.QtCore import QDate, QDateTime, QTime, Qt
 from qgis.PyQt.QtGui import QColor, QImage
 from qgis.testing import unittest
 from test_qgsserver import QgsServerTestBase
@@ -2891,6 +2894,10 @@ def test_temporal_properties(self):
         )
 
         project = QgsProject()
+
+        # Set a filename to avoid capabilities cache breaking test
+        project.setFileName("test_temporal_properties")
+
         project.addMapLayers([layer_date, layer_range])
 
         qs = "?" + "&".join(
@@ -3018,6 +3025,156 @@ def test_temporal_properties(self):
         self.assertEqual(range_extent.attrib, {"name": "TIME"})
         self.assertEqual(range_extent.text, "2003-03-03/2004-04-04")
 
+    def test_get_capabilities_time_dimension(self):
+        """Test if get capabilities return correct time dimension"""
+
+        # Test get capabilities
+        qs = "?" + "&".join(
+            [
+                "%s=%s" % i
+                for i in list(
+                    {
+                        "SERVICE": "WMS",
+                        "VERSION": "1.3.0",
+                        "REQUEST": "GetCapabilities",
+                    }.items()
+                )
+            ]
+        )
+
+        rl1 = QgsRasterLayer(
+            self.get_test_data_path("raster/byte.tif").as_posix(), "test_date_1"
+        )
+        rl2 = QgsRasterLayer(
+            self.get_test_data_path("raster/byte.tif").as_posix(), "test_date_2"
+        )
+        rl3 = QgsRasterLayer(
+            self.get_test_data_path("raster/byte.tif").as_posix(), "test_date_3"
+        )
+        rl4 = QgsRasterLayer(
+            self.get_test_data_path("raster/byte.tif").as_posix(), "test_date_4"
+        )
+        for rl in [rl1, rl2, rl3, rl4]:
+            timeProps = rl.temporalProperties()
+            timeProps.setIsActive(True)
+            timeProps.setMode(Qgis.RasterTemporalMode.FixedTemporalRange)
+
+        rl1.temporalProperties().setFixedTemporalRange(
+            QgsDateTimeRange(
+                QDateTime.fromString("2025-01-12T12:34:56", Qt.ISODate),
+                QDateTime.fromString("2025-01-15T09:12:34", Qt.ISODate),
+            )
+        )
+
+        rl2.temporalProperties().setFixedTemporalRange(
+            QgsDateTimeRange(
+                QDateTime.fromString("2025-01-12T00:00:00", Qt.ISODate),
+                QDateTime.fromString("2025-01-12T00:00:00", Qt.ISODate),
+            )
+        )
+
+        rl3.temporalProperties().setFixedTemporalRange(
+            QgsDateTimeRange(
+                QDateTime.fromString("2025-01-13T00:00:00", Qt.ISODate),
+                QDateTime.fromString("2025-01-13T00:00:00", Qt.ISODate),
+            )
+        )
+
+        project = QgsProject()
+
+        # Set a filename to avoid capabilities cache breaking test
+        project.setFileName("test_get_capabilities_time_dimension")
+
+        project.addMapLayers([rl1, rl2, rl3, rl4], False)
+
+        groupWithTimeDim = project.layerTreeRoot().addGroup("GroupWithTimeDimension")
+        groupWithTimeDim.setHasWmsTimeDimension(True)
+        groupWithoutTimeDim = project.layerTreeRoot().addGroup(
+            "GroupWithoutTimeDimension"
+        )
+
+        groupWithTimeDim.addLayer(rl1)
+        group = groupWithTimeDim.addGroup("SubGroupWithTimeDimension")
+        group.setHasWmsTimeDimension(True)
+        group.addLayer(rl2)
+        group.addLayer(rl4)
+        groupWithTimeDim.addGroup("SubGroupWithoutTimeDimension").addLayer(rl3)
+
+        groupWithoutTimeDim.addLayer(rl1)
+        group = groupWithoutTimeDim.addGroup("OtherSubGroupWithTimeDimension")
+        group.setHasWmsTimeDimension(True)
+        group.addLayer(rl2)
+        group.addLayer(rl4)
+        groupWithoutTimeDim.addGroup("OtherSubGroupWithoutTimeDimension").addLayer(rl3)
+
+        def get_time_dim(layer_name):
+            r, h = self._result(self._execute_request_project(qs, project))
+            t = et.fromstring(r)
+            ns = t.nsmap
+            del ns[None]
+            ns["wms"] = "http://www.opengis.net/wms"
+
+            dims = t.xpath(
+                f"//wms:Layer/wms:Name[text()='{layer_name}']/../wms:Dimension",
+                namespaces=ns,
+            )
+
+            return dims[0] if dims else None
+
+        # range date time
+        date_dimension = get_time_dim("test_date_1")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(date_dimension.text, "2025-01-12T12:34:56/2025-01-15T09:12:34")
+
+        # instant date
+        date_dimension = get_time_dim("test_date_2")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(date_dimension.text, "2025-01-12")
+
+        # instant date
+        date_dimension = get_time_dim("test_date_3")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(date_dimension.text, "2025-01-13")
+
+        # date time not set (expect a time Dimension but no value)
+        date_dimension = get_time_dim("test_date_4")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(date_dimension.text, None)
+
+        # group without time dimension option
+        date_dimension = get_time_dim("GroupWithoutTimeDimension")
+        self.assertEqual(date_dimension, None)
+
+        # group with time dimension option
+        date_dimension = get_time_dim("GroupWithTimeDimension")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(
+            date_dimension.text,
+            "2025-01-12T12:34:56/2025-01-15T09:12:34,2025-01-12T00:00:00",
+        )
+
+        # Test different cases for group recursivity
+
+        date_dimension = get_time_dim("SubGroupWithTimeDimension")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(
+            date_dimension.text,
+            "2025-01-12T00:00:00",
+        )
+
+        date_dimension = get_time_dim("SubGroupWithoutTimeDimension")
+        self.assertEqual(date_dimension, None)
+
+        date_dimension = get_time_dim("OtherSubGroupWithTimeDimension")
+        self.assertEqual(date_dimension.attrib, {"units": "ISO8601", "name": "TIME"})
+        self.assertEqual(
+            date_dimension.text,
+            "2025-01-12T00:00:00",
+        )
+
+        date_dimension = get_time_dim("OtherSubGroupWithoutTimeDimension")
+        self.assertEqual(date_dimension, None)
+
     def test_get_map_labeling_opacities(self):
         """Test if OPACITIES is also applied to labels"""
 


Reviews:
user: login: ptitjano
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T15:11:11Z
user: login: troopa81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:41:23Z
user: login: troopa81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:41:41Z
user: login: troopa81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:41:50Z
user: login: ptitjano
body: Looks good to me. Thanks @troopa81 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T10:58:45Z

Review comments:
diff_hunk: @@ -10,13 +10,22 @@
   </property>
   <property name="windowIcon">
    <iconset>
-    <normaloff/>
-   </iconset>
+    <normaloff>.</normaloff>.</iconset>
path: src/ui/qgsgroupwmsdatadialogbase.ui
user: login: ptitjano
body: why this change?
created_at: 2025-05-05T14:31:55Z
author_association: COLLABORATOR
diff_hunk: @@ -92,6 +92,28 @@ class GUI_EXPORT QgsGroupWmsDataDialog : public QDialog, private Ui::QgsGroupWMS
      */
     Q_DECL_DEPRECATED void setGroupAbstract( const QString &abstract ) SIP_DEPRECATED;
 
+    /**
+     * Set whether the time dimension should be computed for this group or not
path: src/gui/qgsgroupwmsdatadialog.h
user: login: ptitjano
body: Should be `Sets`
created_at: 2025-05-05T14:34:06Z
author_association: COLLABORATOR
diff_hunk: @@ -287,6 +287,27 @@ class CORE_EXPORT QgsLayerTreeGroup : public QgsLayerTreeNode
      */
     const QgsMapLayerServerProperties *serverProperties() const SIP_SKIP;
 
+    /**
+     * Set whether the WMS time dimension should be computed for this group or not
path: src/core/layertree/qgslayertreegroup.h
user: login: ptitjano
body: `Sets whether`
created_at: 2025-05-05T14:34:24Z
author_association: COLLABORATOR
diff_hunk: @@ -1313,61 +1360,13 @@ namespace QgsWms
                && l->temporalProperties()
                && l->temporalProperties()->isActive() )
           {
-            QDomElement dimElem = doc.createElement( QStringLiteral( "Dimension" ) );
-            dimElem.setAttribute( QStringLiteral( "name" ), QStringLiteral( "TIME" ) );
-            dimElem.setAttribute( QStringLiteral( "units" ), QStringLiteral( "ISO8601" ) );
-
             // TODO: set "default" (reference value)
 
             // Add all values
             const QList<QgsDateTimeRange> allRanges { l->temporalProperties()->allTemporalRanges( l ) };
+            bool isDateList = writeTimeDimensionNode( doc, layerElem, allRanges );
path: src/server/services/wms/qgswmsgetcapabilities.cpp
user: login: ptitjano
body: `const bool isDateList`
created_at: 2025-05-05T14:42:10Z
author_association: COLLABORATOR
diff_hunk: @@ -1082,7 +1087,42 @@ namespace QgsWms
       return styleElem;
     }
 
-    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings )
+    //! Return TRUE if date only have been written, FALSE if there is datetime
+    bool writeTimeDimensionNode( QDomDocument &doc, QDomElement &layerElem, const QList<QgsDateTimeRange> &dateRanges )
+    {
+      // Apparently, for vectors allTemporalRanges is always empty :/
+      // there is no way to know the type of range or the individual instants
+
+      // we write a TIME dimension even if dateRanges is empty. Not sure this is appropriate but
+      // it was like that from the beginning so better keep it that way to avoid regression on client side
+
+      const bool hasDateTime = std::any_of( dateRanges.constBegin(), dateRanges.constEnd(), []( const QgsDateTimeRange &r ) { return r.begin().time() != QTime( 0, 0 )
+                                                                                                                                     || ( !r.isInstant() && r.end().time() != QTime( 0, 0 ) ); } );
+
+      const QString dateFormat = hasDateTime ? QStringLiteral( "yyyy-MM-ddTHH:mm:ss" ) : QStringLiteral( "yyyy-MM-dd" );
+
+      QStringList strValues;
+      for ( const QgsDateTimeRange &r : dateRanges )
path: src/server/services/wms/qgswmsgetcapabilities.cpp
user: login: ptitjano
body: could you please rename the variable to avoid single length?
created_at: 2025-05-05T15:01:14Z
author_association: COLLABORATOR
diff_hunk: @@ -18,6 +18,9 @@
  *   (at your option) any later version.                                   *
  *                                                                         *
  ***************************************************************************/
+
+#include <optional>
path: src/server/services/wms/qgswmsgetcapabilities.cpp
user: login: ptitjano
body: I don't think that this include is needed.

created_at: 2025-05-05T15:11:02Z
author_association: COLLABORATOR
diff_hunk: @@ -10,13 +10,22 @@
   </property>
   <property name="windowIcon">
    <iconset>
-    <normaloff/>
-   </iconset>
+    <normaloff>.</normaloff>.</iconset>
path: src/ui/qgsgroupwmsdatadialogbase.ui
user: login: troopa81
body: Don't know. removed
created_at: 2025-05-06T07:41:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,9 @@
  *   (at your option) any later version.                                   *
  *                                                                         *
  ***************************************************************************/
+
+#include <optional>
path: src/server/services/wms/qgswmsgetcapabilities.cpp
user: login: troopa81
body: No, it's not indeed
created_at: 2025-05-06T07:41:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1082,7 +1087,42 @@ namespace QgsWms
       return styleElem;
     }
 
-    void appendLayersFromTreeGroup( QDomDocument &doc, QDomElement &parentLayer, QgsServerInterface *serverIface, const QgsProject *project, const QgsWmsRequest &request, const QgsLayerTreeGroup *layerTreeGroup, const QMap<QString, QgsWmsLayerInfos> &wmsLayerInfos, bool projectSettings )
+    //! Return TRUE if date only have been written, FALSE if there is datetime
+    bool writeTimeDimensionNode( QDomDocument &doc, QDomElement &layerElem, const QList<QgsDateTimeRange> &dateRanges )
+    {
+      // Apparently, for vectors allTemporalRanges is always empty :/
+      // there is no way to know the type of range or the individual instants
+
+      // we write a TIME dimension even if dateRanges is empty. Not sure this is appropriate but
+      // it was like that from the beginning so better keep it that way to avoid regression on client side
+
+      const bool hasDateTime = std::any_of( dateRanges.constBegin(), dateRanges.constEnd(), []( const QgsDateTimeRange &r ) { return r.begin().time() != QTime( 0, 0 )
+                                                                                                                                     || ( !r.isInstant() && r.end().time() != QTime( 0, 0 ) ); } );
+
+      const QString dateFormat = hasDateTime ? QStringLiteral( "yyyy-MM-ddTHH:mm:ss" ) : QStringLiteral( "yyyy-MM-dd" );
+
+      QStringList strValues;
+      for ( const QgsDateTimeRange &r : dateRanges )
path: src/server/services/wms/qgswmsgetcapabilities.cpp
user: login: troopa81
body: :+1: 
created_at: 2025-05-06T07:41:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-10T15:11:43Z
body: <!-- !!## stickymarker ##!! -->

<!-- ====startsection mingw64-qt6-->
### 🪟 Windows Qt6 builds
Download [Windows Qt6 builds of this PR for testing](https://github.com/qgis/QGIS/actions/runs/14854259635/artifacts/3068341121).
*(Built from commit 39560db614d3c07de5f7bbd8cd0e3e8b0bf868d7)*

<!-- ====endsection mingw64-qt6-->

<!-- ====startsection mingw64-->
### 🪟 Windows builds
Download [Windows builds of this PR for testing](https://github.com/qgis/QGIS/actions/runs/14854259643/artifacts/3068455589).
Debug symbols for this build are available [here](https://github.com/qgis/QGIS/actions/runs/14854259643/artifacts/3068456023).
*(Built from commit 39560db614d3c07de5f7bbd8cd0e3e8b0bf868d7)*

<!-- ====endsection mingw64-->

author_association: CONTRIBUTOR
user: login: troopa81
created_at: 2025-04-16T11:42:21Z
body: @elpaso May I request a review here, as you were the first to bring temporal properties to the server #48497
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-01T03:02:26Z
body: The QGIS project highly values your contribution and would love to see this work merged! Unfortunately this PR has not had any activity in the last 14 days and is being automatically marked as "stale". If you think this pull request should be merged, please check
- that all unit tests are passing
- that all comments by reviewers have been addressed
- that there is enough information for reviewers, in particular

  - link to any issues which this pull request fixes

  - add a description of workflows which this pull request fixes

  - add screenshots if applicable

- that you have written unit tests where possible
In case you should have any uncertainty, please leave a comment and we will be happy to help you proceed with this pull request.
If there is no further activity on this pull request, it will be closed in a week.
author_association: CONTRIBUTOR
user: login: troopa81
created_at: 2025-05-06T07:42:08Z
body: @ptitjano comments treated, thanks for the review
author_association: CONTRIBUTOR
user: login: qgis-bot
created_at: 2025-05-07T12:59:22Z
body: @troopa81

This pull request has been tagged for the [changelog](https://www.qgis.org/en/site/forusers/visualchangelogs.html).

* The description will be harvested so please provide a "nearly-ready" text for the final changelog
* If possible, add a nice illustration of the feature. Only the **first** one in the description will be harvested (GIF accepted as well)
* If you can, it's better to give credits to your sponsor, see below for different formats.

You can edit the description.

<details>
<summary>Format available for credits</summary>
<br />

- `Funded by NAME`
- `Funded by URL`
- `Funded by NAME URL`
- `Sponsored by NAME`
- `Sponsored by URL`
- `Sponsored by NAME URL`

</details>

Thank you!
author_association: COLLABORATOR
user: login: qgis-bot
created_at: 2025-06-17T16:11:18Z
body: @troopa81
This pull request has been tagged as **requiring documentation**.

A documentation ticket will be opened at https://github.com/qgis/QGIS-Documentation **when this PR is merged**.

**Please update the description** (not the comments) with helpful description and screenshot to help the work from documentors.
Also, any commit having [needs-doc] or [Needs Documentation] in will see its message pushed to the issue, so please be as verbose as you can.

Thank you!
author_association: COLLABORATOR
user: login: qgis-bot
created_at: 2025-06-17T16:11:19Z
body: @troopa81
A documentation ticket has been opened at https://github.com/qgis/QGIS-Documentation/issues/9968
It is **your** responsibility to visit this ticket and add as much detail as possible for the documentation team to correctly document this change.
Thank you!
author_association: COLLABORATOR