Permalink
Browse files

Implement support for millisecond and microsecond times

This also deduces the time format rather than specifying it.  Time parsing
tries all the supported formats and saves the one that works to minimize
the try/except exception handling overhead.  The nice thing about this
is it makes it unnecessary to specify the time format when configuring
the TimeManager settings.

The current time setting in the project file gets truncated to the
previous second mark.  This is slightly annoying, but not annoying enough
for yours truly to fix.

Signed-off-by: Gerald Van Baren <gvb@unssw.com>
  • Loading branch information...
1 parent 919a0af commit be56147336215068478e676ac9ee57ab023c4b42 @gvb gvb committed Jan 25, 2012
Showing with 57 additions and 44 deletions.
  1. +3 −0 README.md
  2. +2 −18 addLayer.ui
  3. +1 −1 dockwidget2.ui
  4. +24 −2 timelayer.py
  5. +22 −10 timelayermanager.py
  6. +1 −1 timemanagercontrol.py
  7. +4 −12 timemanagerguicontrol.py
View
@@ -15,10 +15,13 @@ The aim of '''Time Manager plugin for QGIS''' is to provide comfortable browsing
Time Manager filters your vector datasets (It only works for vector data!) and displays only features with timestamps in the user specified time frame. Timestamps have to be in one of the following formats:
+* YYYY-MM-DD HH:MM:SS.ssssss
* YYYY-MM-DD HH:MM:SS
* YYYY-MM-DD HH:MM
* YYYY-MM-DD
+The list of supported time formats can augmented by adding to `supportedFormats` in `timelayer.py`
+
The biggest tested dataset was a Spatialite table with indexed timestamps containing approximately 400,000 points, covering a time span of 24 hours. Stepping through the data for example in 1-hour-sized steps works without problems.
Time Manager 0.3 supports exporting image series based on the defined animation settings. Our goal for future versions is to include a tool that creates actual animations from these image series. Until then, external programs can be used for this last step. A good option is memcoder. This is how it's used to create an .avi from all images within a folder:
View
@@ -54,29 +54,13 @@
</widget>
</item>
<item row="3" column="0">
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string>Time Format:</string>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <widget class="QComboBox" name="comboBoxTimeFormat">
- <item>
- <property name="text">
- <string>%Y-%m-%d %H:%M:%S</string>
- </property>
- </item>
- </widget>
- </item>
- <item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Offset (in sec):</string>
</property>
</widget>
</item>
- <item row="4" column="1">
+ <item row="3" column="1">
<widget class="QSpinBox" name="spinBoxOffset">
<property name="minimum">
<number>-1000000000</number>
@@ -86,7 +70,7 @@
</property>
</widget>
</item>
- <item row="4" column="2">
+ <item row="3" column="2">
<widget class="QLabel" name="label_7">
<property name="text">
<string>(optional)</string>
View
@@ -122,7 +122,7 @@
<item>
<widget class="QDateTimeEdit" name="dateTimeEditCurrentTime">
<property name="displayFormat">
- <string>yyyy-MM-dd HH:mm:ss</string>
+ <string>yyyy-MM-dd HH:mm:ss.zzz</string>
</property>
</widget>
</item>
View
@@ -15,6 +15,13 @@ def __init__(self,layer,fromTimeAttribute,toTimeAttribute,enabled=True,timeForma
self.timeEnabled = enabled
self.originalSubsetString = self.layer.subsetString()
self.timeFormat = str(timeFormat) # cast in case timeFormat comes as a QString
+ self.supportedFormats = [
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d %H:%M:%S.%f",
+ "%Y-%m-%d %H:%M",
+ "%Y-%m-%d"]
+ if timeFormat not in self.supportedFormats:
+ self.supportedFormats.append(timeFormat)
self.offset = int(offset)
try:
self.getTimeExtents()
@@ -52,6 +59,21 @@ def getOffset(self):
"""returns the layer's offset, integer in seconds"""
return self.offset
+ def strToDatetime(self, dtStr):
+ """convert a date/time string into a Python datetime object"""
+ try:
+ # Try the last known format, if not, try all known formats.
+ return datetime.strptime(dtStr, self.timeFormat)
+ except:
+ for fmt in self.supportedFormats:
+ try:
+ self.timeFormat = fmt
+ return datetime.strptime(dtStr, self.timeFormat)
+ except:
+ pass
+ # If all fail, re-raise the exception
+ raise
+
def getTimeExtents( self ):
"""Get layer's temporal extent using the fields and the format defined somewhere else!"""
#In irgendeiner Weise sollte der Benutzer auch erahnen können, wenn er Datensätze hat, die kein gültiges Datum haben, und daher nie angezeigt würden.
@@ -61,11 +83,11 @@ def getTimeExtents( self ):
startStr = str(provider.minimumValue(fromTimeAttributeIndex).toString())
endStr = str(provider.maximumValue(toTimeAttributeIndex).toString())
try:
- startTime = datetime.strptime(startStr,self.timeFormat)
+ startTime = self.strToDatetime(startStr)
except ValueError:
raise NotATimeAttributeError(str(self.getName())+': The attribute specified for use as start time contains invalid data.')
try:
- endTime = datetime.strptime(endStr,self.timeFormat)
+ endTime = self.strToDatetime(endStr)
except ValueError:
raise NotATimeAttributeError(str(self.getName())+': The attribute specified for use as end time contains invalid data.')
# apply offset
View
@@ -187,7 +187,7 @@ def setCurrentTimePosition( self, timePosition ):
# TODO: Test
if type(timePosition) == QDateTime:
# convert QDateTime to datetime
- timePosition = datetime.strptime( str(timePosition.toString('yyyy-MM-dd hh:mm:ss')) ,"%Y-%m-%d %H:%M:%S")
+ timePosition = datetime.strptime( str(timePosition.toString('yyyy-MM-dd hh:mm:ss.zzz')) ,"%Y-%m-%d %H:%M:%S.%f")
elif type(timePosition) == int or type(timePosition) == float:
timePosition = datetime.fromordinal(int(timePosition))
self.currentTimePosition = timePosition
@@ -231,28 +231,40 @@ def deactivateTimeManagement(self):
def getSaveString(self):
"""create a save string that can be put into project file"""
+ tdfmt = "%Y-%m-%d %H:%M:%S.%f"
saveString = ''
saveListLayers = QStringList()
if len(self.projectTimeExtents) > 0:
- saveString = str(self.projectTimeExtents[0]) + ';' + str(self.projectTimeExtents[1]) + ';'
- saveString += str(self.currentTimePosition) + ';'
+ saveString = datetime.strftime(self.projectTimeExtents[0], tdfmt) + ';'
+ saveString += datetime.strftime(self.projectTimeExtents[1], tdfmt) + ';'
+ saveString += datetime.strftime(self.currentTimePosition, tdfmt) + ';'
for timeLayer in self.timeLayerList:
saveListLayers.append(timeLayer.getSaveString())
return (saveString,saveListLayers)
- def restoreFromSaveString(self,saveString):
+ def restoreFromSaveString(self, saveString):
"""restore settings from loaded project file"""
+ tdfmt = "%Y-%m-%d %H:%M:%S.%f"
if saveString:
self.isFirstRun = False
- saveString = str(saveString)
- saveString = saveString.split(';')
+ saveString = str(saveString).split(';')
try:
- timeExtents = (datetime.strptime(saveString[0],"%Y-%m-%d %H:%M:%S"),datetime.strptime(saveString[1],"%Y-%m-%d %H:%M:%S"))
- except ValueError: # avoid error message for projects without time-managed layers
- return
+ timeExtents = (datetime.strptime(saveString[0], tdfmt),
+ datetime.strptime(saveString[1], tdfmt))
+ except ValueError:
+ try:
+ # Try converting without the fractional seconds for
+ # backward compatibility.
+ tdfmt = "%Y-%m-%d %H:%M:%S"
+ timeExtents = (datetime.strptime(saveString[0], tdfmt),
+ datetime.strptime(saveString[1], tdfmt))
+ except ValueError:
+ # avoid error message for projects without
+ # time-managed layers
+ return
self.projectTimeExtents = timeExtents
- self.setCurrentTimePosition(datetime.strptime(saveString[2],"%Y-%m-%d %H:%M:%S"))
+ self.setCurrentTimePosition(datetime.strptime(saveString[2], tdfmt))
return saveString[3]
@@ -209,7 +209,7 @@ def setCurrentTimePosition(self,timePosition):
original = timePosition
if type(timePosition) == QDateTime:
# convert QDateTime to datetime :S
- timePosition = datetime.strptime( str(timePosition.toString('yyyy-MM-dd hh:mm:ss')) ,"%Y-%m-%d %H:%M:%S")
+ timePosition = datetime.strptime( str(timePosition.toString('yyyy-MM-dd hh:mm:ss.zzz')) ,"%Y-%m-%d %H:%M:%S.%f")
elif type(timePosition) == int or type(timePosition) == float:
timePosition = datetime.fromtimestamp(timePosition)
if timePosition == self.currentMapTimePosition:
@@ -36,10 +36,6 @@ def __init__ (self,iface,timeLayerManager):
self.dock = uic.loadUi( os.path.join( path, "dockwidget2.ui" ) )
self.iface.addDockWidget( Qt.BottomDockWidgetArea, self.dock )
- # remove micro and milliseconds until supported
- self.dock.comboBoxTimeExtent.removeItem(0) # microseconds
- self.dock.comboBoxTimeExtent.removeItem(0) # milliseconds
-
self.dock.pushButtonExportVideo.setEnabled(False) # only enabled if there are managed layers
self.dock.comboBoxTimeExtent.setCurrentIndex(3) # should be 'days'
@@ -232,10 +228,6 @@ def showAddLayerDialog(self):
# get attributes of the first layer for gui initialization
self.getLayerAttributes(0)
- # add additional supported time formats
- supportedFormats=["%Y-%m-%d %H:%M","%Y-%m-%d"] # additional formats, the only default is %Y-%m-%d %H:%M:%S
- self.addLayerDialog.comboBoxTimeFormat.addItems(supportedFormats)
-
self.addLayerDialog.show()
# establish connections
@@ -279,7 +271,7 @@ def addLayerToOptions(self):
endTime = self.addLayerDialog.comboBoxEnd.currentText()
checkState = Qt.Checked
layerId = self.layerIds[self.addLayerDialog.comboBoxLayers.currentIndex()]
- timeFormat = self.addLayerDialog.comboBoxTimeFormat.currentText()
+ timeFormat = "%Y-%m-%d %H:%M:%S" # default
offset = self.addLayerDialog.spinBoxOffset.value()
self.addRowToOptionsTable(layerName,startTime,endTime,checkState,layerId,timeFormat,offset)
@@ -322,8 +314,8 @@ def addRowToOptionsTable(self,layerName,startTime,endTime,checkState,layerId,tim
def updateTimeExtents(self,timeExtents):
"""update time extents showing in labels and represented by horizontalTimeSlider"""
if timeExtents != (None,None):
- self.dock.labelStartTime.setText(str(timeExtents[0]))
- self.dock.labelEndTime.setText(str(timeExtents[1]))
+ self.dock.labelStartTime.setText(str(timeExtents[0])[0:23])
+ self.dock.labelEndTime.setText(str(timeExtents[1])[0:23])
self.dock.horizontalTimeSlider.setMinimum(mktime(timeExtents[0].timetuple()))
self.dock.horizontalTimeSlider.setMaximum(mktime(timeExtents[1].timetuple()))
else: # set to default values
@@ -382,7 +374,7 @@ def renderLabel(self, painter):
return
self.font = QFont("Arial")
- self.labelString = str(self.dock.dateTimeEditCurrentTime.dateTime().toString("yyyy-MM-dd hh:mm:ss"))
+ self.labelString = str(self.dock.dateTimeEditCurrentTime.dateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"))
self.placementIndex = 3
fm = QFontMetrics(self.font, painter.device())

0 comments on commit be56147

Please sign in to comment.