diff --git a/README.md b/README.md index a869550..487a4f0 100644 --- a/README.md +++ b/README.md @@ -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/ + ## What is the goal diff --git a/index.html b/index.html index 4102e36..8b3cec7 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Timemanager by anitagraser + Timemanager by anitagraser, Karolina Alexiou and Seyed Javad Adabikhosh @@ -41,7 +41,7 @@

QGIS Time Manager Plugin

 


Project maintained by anitagraser + href="https://github.com/javadadabi">Seyed Javad Adabikhosh Hosted on GitHub Pages — Theme by mattgraham @@ -63,7 +63,7 @@

What Time Manager does

get accustomed to Time Manager.

More information on functionality and limitations can be found on Github.

+ href="https://github.com/javadadabi/TimeManager/tree/timemanager/#readme">Github.

Where to download Time Manager

diff --git a/layers/timevectorlayer.py b/layers/timevectorlayer.py index 5bcedcc..0504413 100644 --- a/layers/timevectorlayer.py +++ b/layers/timevectorlayer.py @@ -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)) diff --git a/metadata.txt b/metadata.txt index cba627a..ae21c9d 100644 --- a/metadata.txt +++ b/metadata.txt @@ -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 diff --git a/raster/cdflayer.py b/raster/cdflayer.py index c4fa854..2bc34f2 100644 --- a/raster/cdflayer.py +++ b/raster/cdflayer.py @@ -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 @@ -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)) @@ -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 @@ -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: @@ -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] @@ -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): diff --git a/raster/wmstlayer.py b/raster/wmstlayer.py index ce110d5..2309f91 100644 --- a/raster/wmstlayer.py +++ b/raster/wmstlayer.py @@ -1,4 +1,5 @@ from future import standard_library + standard_library.install_aliases() # -*- coding: utf-8 -*- @@ -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 @@ -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 "?" @@ -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() - diff --git a/timemanager_obj.py b/timemanager_obj.py index ff72af2..c89fa7c 100644 --- a/timemanager_obj.py +++ b/timemanager_obj.py @@ -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 @@ -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): diff --git a/timemanagercontrol.py b/timemanagercontrol.py index 2e8af00..e6f4811 100644 --- a/timemanagercontrol.py +++ b/timemanagercontrol.py @@ -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""" diff --git a/utils/time_util.py b/utils/time_util.py index 2b2cba5..280eb15 100644 --- a/utils/time_util.py +++ b/utils/time_util.py @@ -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():