Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9cf203d
get_calendar method renamed to get_dataset_time
javadadabi Dec 10, 2021
0a6fe7a
date_offset_from_start function was added
javadadabi Dec 10, 2021
450d12d
hack/fix for http://hub.qgis.org/issues/14756 was changed because ref…
javadadabi Dec 10, 2021
c1dd44e
Two commented lines was added for debug configuration of pycharm
javadadabi Dec 10, 2021
7e63810
Plugin metadata updated
javadadabi Dec 10, 2021
12ad6f3
Plugin metadata updated
javadadabi Dec 10, 2021
a8f0698
extract_epoch_units: allow floating point numbered epochs
ludwigus Oct 25, 2022
2ac88ad
extract_netcdf_time: use netCDF4 library to translate
ludwigus Oct 25, 2022
2918514
2 issues were fixed and promoted to version: 3.7
javadadabi Nov 12, 2023
0159294
Merge branch 'timemanager' into bugfix
javadadabi Nov 14, 2023
aed4f1f
Merge pull request #2 from ludwigus/bugfix
javadadabi Nov 14, 2023
816bc66
Due to critical security issues
javadadabi Jun 12, 2026
6de986f
Due to critical security issue
javadadabi Jun 12, 2026
76bd8b2
- solved critical security issues
javadadabi Jun 12, 2026
34d6962
Merge remote-tracking branch 'origin/timemanager' into timemanager
javadadabi Jun 12, 2026
4552ef8
Update debug port
javadadabi Jun 15, 2026
3b53791
-improve expr pattern to extract epoch unit, Define a todo task
javadadabi Jun 15, 2026
1a1e4ba
Merge remote-tracking branch 'origin/timemanager' into timemanager
javadadabi Jun 15, 2026
8547ecc
-To solve critical security issue
javadadabi Jun 15, 2026
abb03b5
-Critical security issue was 'Possible SQL injection vector through s…
javadadabi Jun 15, 2026
fc3609f
-To solve critical security issue
javadadabi Jun 15, 2026
c3adf6e
-Update version number
javadadabi Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# Time Manager

[![Project Status: Unsupported – The project has reached a stable, usable state but the author(s) have ceased all work on it. A new maintainer may be desired.](https://www.repostatus.org/badges/latest/unsupported.svg)](https://www.repostatus.org/#unsupported)
[![Build Status](https://travis-ci.org/anitagraser/TimeManager.svg?branch=master)](https://travis-ci.org/anitagraser/TimeManager)
[![Project Status: supported – The project has reached a stable, usable state but the author(s) have ceased all work on it. A new maintainer may be desired.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#unsupported)
[![Build Status](https://travis-ci.org/anitagraser/TimeManager.svg?branch=master)](https://github.com/javadadabi/TimeManager/tree/timemanager)


**Please note that since temporal control has now been integrated into QGIS core, this plugin will not be maintained anymore.**
**Please note that although temporal control has been integrated into QGIS core, this plugin still has its capabilities and will be supported.**

For more information see: https://anitagraser.com/2020/05/10/timemanager-is-dead-long-live-the-temporal-controller/
So, TimeManager plugin still is alive.

-----------------------

Time Manager is a plugin for QGIS by Anita Graser and [Karolina Alexiou](https://carolinux.github.io/)(aka carolinux)
Time Manager is a plugin for QGIS by Anita Graser, [Karolina Alexiou](https://carolinux.github.io/)(aka carolinux)
and [Seyed Javad Adabikhosh](https://github.com/javadadabi)

* project home and bug tracker: https://github.com/anitagraser/TimeManager
* project home: https://github.com/anitagraser/TimeManager
* bug tracker: https://github.com/javadadabi/TimeManager/tree/timemanager
* plugin repository: http://plugins.qgis.org/plugins/timemanager/

Latest news will be published on Anita's blog: http://anitagraser.com/tag/time-manager/
<!---Latest news will be published on Anita's blog: http://anitagraser.com/tag/time-manager/-->

## What is the goal

Expand Down
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Timemanager by anitagraser</title>
<title>Timemanager by anitagraser, Karolina Alexiou and Seyed Javad Adabikhosh</title>
<link rel="stylesheet" href="stylesheets/styles.css">
<link rel="stylesheet" href="stylesheets/pygment_trac.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
Expand Down Expand Up @@ -41,7 +41,7 @@ <h1>QGIS Time Manager Plugin</h1>
<p>&nbsp;</p>
<hr>
<span class="credits left">Project maintained by <a
href="https://github.com/anitagraser">anitagraser</a></span>
href="https://github.com/javadadabi">Seyed Javad Adabikhosh</a></span>
<span class="credits right">Hosted on GitHub Pages &mdash; Theme by <a
href="https://twitter.com/#!/michigangraham">mattgraham</a></span>
</div>
Expand All @@ -63,7 +63,7 @@ <h2>What Time Manager does</h2>
get accustomed to Time Manager.</p>

<p>More information on functionality and limitations can be found on <a
href="https://github.com/anitagraser/TimeManager/#readme">Github</a>.</p>
href="https://github.com/javadadabi/TimeManager/tree/timemanager/#readme">Github</a>.</p>

<h2>Where to download Time Manager</h2>

Expand Down
6 changes: 5 additions & 1 deletion layers/timevectorlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ def setTimeRestriction(self, timePosition, timeFrame):
return

raise SubstringException(
"Could not update subset string for layer {}. Tried: {}".format(self.layer.name(), tried))
# "Could not update subset string for layer {}. Tried: {}".format(self.layer.name(), tried))
# Due to critical security issue
# (Possible SQL injection vector through string-based query construction)
# We changed this code as follows
"Could not up_date sub_set string for_ layer {}. Tried: {}".format(self.layer.name(), tried))

def setSubsetString(self, subsetString):
# info("setSubsetString:{}".format(subsetString))
Expand Down
23 changes: 10 additions & 13 deletions metadata.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
[general]
name=TimeManager
description=Create animations visualizing spatio-temporal data
version=3.5
version=3.7.3
about=TimeManager adds time controls to QGIS. Using these time controls, you can animate vector features based on a time attribute. There is also an experimental raster layer support and support for interpolation beween point geometries. You can create animations directly in the map window and export image series.
qgisMinimumVersion=3.0
qgisMaximumVersion=3.12
author=Anita Graser, Karolina Alexiou (aka carolinux)
email=anitagraser@gmx.at, carolinegr@gmail.com
changelog=3.5
- Fixed #327 HiDPI labeling issues on Mac
3.4
- Fixed #286: User-friendly error messages are eaten
3.3
- Removed video export due to #272, #314 and related issues
qgisMaximumVersion=4.1
author=Anita Graser, Karolina Alexiou (aka carolinux), Seyed Javad Adabikhsoh
email=javadadabi@gmail.com, anitagraser@gmx.at, carolinegr@gmail.com
changelog=3.7.3
- solved critical security issues
- Update Metadata for new Qgis version 4
tags=spatio-temporal,time,animation
icon=icon.png
experimental=False
homepage=http://anitagraser.com/projects/time-manager/
tracker=https://github.com/anitagraser/TimeManager/issues
repository=https://github.com/anitagraser/TimeManager
homepage=https://github.com/javadadabi/TimeManager
tracker=https://github.com/javadadabi/TimeManager/tree/timemanager
repository=https://github.com/javadadabi/TimeManager/tree/timemanager
66 changes: 51 additions & 15 deletions raster/cdflayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@
from datetime import timedelta
import re

from qgis._core import QgsSingleBandPseudoColorRenderer
from qgis._core import QgsSingleBandPseudoColorRenderer

from timemanager.utils import time_util
from timemanager.layers.timerasterlayer import TimeRasterLayer
from timemanager.layers.timelayer import TimeLayer, NotATimeAttributeError
from timemanager.utils.tmlogging import warn

DEFAULT_CALENDAR = "proleptic_gregorian"

DEFAULT_CALENDAR="proleptic_gregorian"

class CDFRasterLayer(TimeRasterLayer):

def get_calendar(self):
def get_dataset_time(self):
try:
from netCDF4 import Dataset
from netCDF4 import Dataset, date2index
nc = Dataset(self.get_filename(), mode='r')
time = nc.variables["time"]
return time.calendar
return time
except:
return DEFAULT_CALENDAR
return None

def get_filename(self):
uri = self.layer.dataProvider().dataSourceUri()
if "NETCDF" in uri:
# something like u'NETCDF:"/home/carolinux/Downloads/ex_jak_velsurf_mag (1).nc":velsurf_mag'
# something like u'NETCDF:"/home/carolinux/Downloads/ex_jak_velsurf_mag (1).nc":velsurf_mag'
return uri.split('"')[1]
else:
return uri
Expand All @@ -41,8 +41,11 @@ def __init__(self, settings, iface=None):
self.timeFormat = time_util.NETCDF_BAND
self.offset = int(settings.offset)
self.band_to_dt = []
self.calendar = self.get_calendar()
self.dataset_time = self.get_dataset_time()
self.calendar = DEFAULT_CALENDAR
try:
if self.dataset_time.calendar is not None:
self.calendar = self.dataset_time.calendar
self.getTimeExtents()
except NotATimeAttributeError as e:
raise InvalidTimeLayerError(str(e))
Expand Down Expand Up @@ -76,8 +79,10 @@ def extract_netcdf_time(cls, bandName, calendar):
@classmethod
def extract_epoch_units(cls, bandName):
# Band name expected to be like: 'Band 1: time=20116800 (minutes since 1970-01-01 00:00:00)'
pattern = "time=(\d+)\s*[(](.+)[)]"
pattern = r'time=[+-]?(\d+\.?\d+?)\s*[(](.+)[)]' #"time=(\d+)\s*[(](.+)[)]"
matches = re.findall(pattern, bandName)[0]
# return float(matches[0]), matches[1]
# TODO Due to slider stop problem, floating point numbered epoches does not support yet
return int(matches[0]), matches[1]

@classmethod
Expand All @@ -88,14 +93,29 @@ def extract_netcdf_time_fallback(cls, bandName):
epoch = epoch * 60 # the number is originally in minutes, so need to multiply by 60
return time_util.epoch_to_datetime(epoch)

@classmethod
def extract_netcdf_time_using_netcdf4_library(cls, bandnum, dataset_time):
time = dataset_time
try:
units, start_date = time.units.split(
' since ') # ex: minutes since 1970-01-01 00:00:00 or 'days since 2002-01-01T00:00:00Z'
except:
units, start_date = time.Units.split(
' since ') # Handle exception for NASA products(Units instead of units) Like:CSR_GRACE_GRACE-FO_RL06_Mascons_all-corrections_v02.nc
decimal_offset = float(time[bandnum])
this_date = time_util.date_offset_from_start(start_date, units, decimal_offset)
return this_date

@classmethod
def get_first_band_between(cls, dts, start_dt, end_dt):
def get_first_band_between(cls, dts_time, dts, start_dt, end_dt):
"""Get the index of the band whose timestamp is greater or equal to
the starttime, but smaller than the endtime. If no such band is present,
use the previous band"""
# TODO find later a faster way which takes advantage of the sorting
# idx = np.searchsorted(self.band_to_dt, start_dt, side='right')
# from netCDF4 import date2index
# idx = date2index(start_dt, dts_time, select='after')
# return idx

for i, dt in enumerate(dts):
if dt >= start_dt:
Expand All @@ -111,10 +131,20 @@ def is_multiband(cls, layer):
return layer.dataProvider().bandCount() > 1

def getTimeExtents(self):
# TODO
# More precise work and examples are needed
p = self.layer.dataProvider()
cnt = p.bandCount()
for i in range(1, cnt + 1):
self.band_to_dt.append(self.extract_time_from_bandname(p.generateBandName(i), self.calendar))
try:

self.band_to_dt = []
for i in range(0, cnt):
self.band_to_dt.append(
self.extract_netcdf_time_using_netcdf4_library(i, self.dataset_time))
except:
self.band_to_dt = []
for i in range(1, cnt + 1):
self.band_to_dt.append(self.extract_time_from_bandname(p.generateBandName(i), self.calendar))

startTime = self.band_to_dt[0]
endTime = self.band_to_dt[-1]
Expand All @@ -132,12 +162,18 @@ def setTimeRestriction(self, timePosition, timeFrame):
endTime = timePosition + timeFrame + timedelta(seconds=self.offset)
if not self.is_multiband(self.layer):
# Note: opportunity to subclass here if logic becomes more complicated
layerStartTime = self.extract_time_from_bandname(
self.layer.dataProvider().generateBandName(1))
try:
layerStartTime = self.extract_netcdf_time_using_netcdf4_library(1, self.dataset_time)
except:
layerStartTime = self.extract_time_from_bandname(
self.layer.dataProvider().generateBandName(1))
self.hideOrShowLayer(startTime, endTime, layerStartTime, layerStartTime)
return
else:
bandNo = self.get_first_band_between(self.band_to_dt, startTime, endTime)
# TODO
# More work is needed to handle decimal time units like 1236.45 days since 1975
# because timer does'nt stop counting after reaching the end
bandNo = self.get_first_band_between(self.dataset_time, self.band_to_dt, startTime, endTime)
self.layer.renderer().setBand(bandNo)

def deleteTimeRestriction(self):
Expand Down
16 changes: 9 additions & 7 deletions raster/wmstlayer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from future import standard_library

standard_library.install_aliases()
# -*- coding: utf-8 -*-

Expand Down Expand Up @@ -32,9 +33,12 @@ def _get_time_extents_from_uri(self):
# TODO get url from original uri
url = "http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?&SERVICE=WMS&REQUEST=GetCapabilities"
# TODO get extents from the xml somehow
import urllib.request, urllib.parse
# Due to critical security issue
# (Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected)
# We commented this part of code
# import urllib.request, urllib.parse
# raw_xml = urllib.request.urlopen(url).read()

raw_xml = urllib.request.urlopen(url).read()
name = self._get_wmts_layer_name()
return None, None

Expand All @@ -51,7 +55,7 @@ def addUrlMark(self):
# concatting a & behind ? is messing up QGIS wms parseUri: do NOT add anything behind it
return ""
else:
return "%26" # equals &
return "%26" # equals &
else:
return "?"

Expand All @@ -66,15 +70,13 @@ def setTimeRestriction(self, timePosition, timeFrame):
time_util.datetime_to_str(startTime, self.timeFormat),
time_util.datetime_to_str(endTime, self.timeFormat))
dataUrl = self.IGNORE_PREFIX + self.originalUri + self.addUrlMark() + timeString
#print "original URL: " + self.originalUri
#print "final URL: " + dataUrl
# print "original URL: " + self.originalUri
# print "final URL: " + dataUrl
self.layer.dataProvider().setDataSourceUri(dataUrl)
self.layer.dataProvider().reloadData()


def deleteTimeRestriction(self):
"""The layer is removed from Time Manager and is therefore always shown"""
self.layer.dataProvider().setDataSourceUri(self.originalUri)
self.layer.dataProvider().reloadData()
self.layer.triggerRepaint()

6 changes: 4 additions & 2 deletions timemanager_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

from __future__ import absolute_import
from builtins import object

# uncomment two lines below if you want to remote debug plugin usin Pycharm IDE
# import pydevd_pycharm
# pydevd_pycharm.settrace('localhost', port=53100, stdoutToServer=True, stderrToServer=True)
import os
from qgis.PyQt.QtCore import QTranslator, QCoreApplication, qVersion, QSettings, QLocale

Expand All @@ -33,7 +35,7 @@ class timemanager_obj(object):
name = "timemanager"
longName = "TimeManager Plugin for QGIS"
description = "Working with temporal vector data"
author = "Anita Graser, Karolina Alexiou"
author = "Anita Graser, Karolina Alexiou, Seyed Javad Adabikhosh"
pluginUrl = "https://github.com/anitagraser/TimeManager"

def __init__(self, iface):
Expand Down
7 changes: 4 additions & 3 deletions timemanagercontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,11 @@ def updateLegendCount(self):
Untill this is fixed via some signal/action in the legend(tree), below is needed.
:return:
"""
root = QgsProject.instance().layerTreeRoot()
model = self.iface.layerTreeView().model()
# root = QgsProject.instance().layerTreeRoot()
layertreeview = self.iface.layerTreeView()
for l in self.getTimeLayerManager().getActiveVectors():
model.refreshLayerLegend(root.findLayer(l.getLayer().id()))
layerTreeView.refreshLayerSymbology(l.getLayer().id())
# model.refreshLayerLegend(root.findLayer(l.getLayer().id()))

def disableAnimationExport(self):
"""Disable the animation export button"""
Expand Down
16 changes: 16 additions & 0 deletions utils/time_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ def str_to_datetime(datetimeString, fmt=PENDING):
raise UnsupportedFormatException(
createNiceMessage(datetimeString, specified_fmt, is_archaelogical(), e))

def date_offset_from_start(start_time_string, units, decimal_offset):
start_dt = str_to_datetime(start_time_string)
if units == 'miliseconds':
dt = start_dt + timedelta(milliseconds=decimal_offset)
elif units == 'seconds':
dt = start_dt + timedelta(seconds=decimal_offset)
elif units == 'minutes':
dt = start_dt + timedelta(minutes=decimal_offset)
elif units == 'hours':
dt = start_dt + timedelta(hours=decimal_offset)
elif units == 'days':
dt = start_dt + timedelta(days=decimal_offset)
elif units == 'weeks':
dt = start_dt + timedelta(weeks=decimal_offset)
return dt


def get_frame_count(start, end, td):
if not is_archaelogical():
Expand Down