0

I am making a scatter plot of positions marked with latitude and longitude which works all right as it shows all the positions in a time period in a static image. But I was wondering if there is any easy way of utilizing that I have the unixtime to every position - so I can show the movements as a timelapse - kind of looping through the positions and showing an animation of the movement.

EDIT:

I have set up a dynamicly updating plot that plots all the positions one by one, now I just have to add the basemap in the background. The codes come from this answer.

import matplotlib.pyplot as plt
import sqlite3 as lite
from operator import itemgetter

def getData():
    con = lite.connect('database.db')
    with con:
        cur = con.cursor()
        cur.execute('SELECT latitude, longitude, unixtime FROM Message WHERE latitude > 50 AND longitude > -30 AND longitude < 40 AND latitude < 80')
        all_rows = [[int(x[0]), int(x[1]), int(x[2])] for x in cur]
        all_rows = sorted(all_rows, key=itemgetter(2))
        return all_rows

plt.ion()
class DynamicUpdate():
    #Suppose we know the x range
    min_x = 0
    max_x = 10000

    def on_launch(self):
        #Set up plot
        self.figure, self.ax = plt.subplots()
        self.lines, = self.ax.plot([],[], 'o')
        #Autoscale on unknown axis and known lims on the other
        self.ax.set_autoscaley_on(True)
        self.ax.set_xlim(-50, 50)
        self.ax.set_ylim(40, 80)
        #Other stuff
        self.ax.grid()

    def on_running(self, xdata, ydata):
        #Update data (with the new _and_ the old points)
        self.lines.set_xdata(xdata)
        self.lines.set_ydata(ydata)
        #Need both of these in order to rescale
        self.ax.relim()
        self.ax.autoscale_view()
        #We need to draw *and* flush
        self.figure.canvas.draw()
        self.figure.canvas.flush_events()

    #Example
    def __call__(self):
        import numpy as np
        import time
        self.on_launch()
        xdata = []
        ydata = []
        all_rows = getData()
        for x in all_rows:
            a,b,f = zip(x)
            xdata.append(b)
            ydata.append(a)
            self.on_running(xdata, ydata)
        return xdata, ydata

d = DynamicUpdate()
d()

Old Code:

This shows the static data

    map = Basemap(projection='merc', lat_0=59.45, lon_0=10.5,
        resolution = 'c', area_thresh = 1000,
    llcrnrlon=-30, llcrnrlat=50,
    urcrnrlon=40, urcrnrlat=80)

    map.drawcoastlines()
    map.fillcontinents(color='black')
    con = lite.connect('database.db')
    with con:
        cur = con.cursor()
        cur.execute('SELECT latitude, longitude FROM Message WHERE latitude > 50 AND longitude > -30 AND longitude < 40 AND latitude < 80')
        data = cur.fetchall()
        y,x = zip(*data)
        x,y = map(x,y)
        plt.scatter(x,y, s=0.07, alpha=0.6, color="#e74c3c", edgecolors='none')
        plt.show()
Community
  • 1
  • 1
bjornasm
  • 2,211
  • 7
  • 37
  • 62

2 Answers2

1

There are are few ways to do animations in matplotlib, the matplotlib.animation provides a framework but this can be a little involved. Probabaly the easiest way to do it is using plt.ion(). I don't know how you access your date with cur.execute but does something like this work:

map = Basemap(projection='merc', lat_0=59.45, lon_0=10.5,
    resolution = 'c', area_thresh = 1000,
llcrnrlon=-30, llcrnrlat=50,
urcrnrlon=40, urcrnrlat=80)

fig, ax = plt.subplots(1,1)
plt.ion()
plt.show()

map.drawcoastlines(ax=ax)
map.fillcontinents(color='black',ax=ax)
con = lite.connect('database.db')

with con:
    cur = con.cursor()
    i=0
    for unixtime in range(1406851200,1409529600):
        cur.execute('SELECT latitude, longitude FROM Message WHERE latitude > 50 AND longitude > -30 AND longitude < 40 AND latitude < 80 AND unixtime ==' + str(unixtime))
        data = cur.fetchall()
        y,x = zip(*data)
        x,y = map(x,y)
        pts = ax.scatter(x,y, s=0.07, alpha=0.6, color="#e74c3c", edgecolors='none')
        plt.draw()
        plt.pause(0.0001)
        #i += 1
        #plt.savefig('./out.{0:07d}.png'.format(i))
        pts.remove() 
Ed Smith
  • 12,716
  • 2
  • 43
  • 55
  • Thank you for your help! It seems though that this plots first the map, then plots the points in an other window - but it seems close! – bjornasm Mar 30 '15 at 16:00
  • 1
    Sorry, didn't realise the drawcoastlines and fillcontinents were part of the plotting function. I've moved the figure generation before and passed the axis handle (ax) to these functions. It's not possible to check if this works without your database.db file but should be something like this. – Ed Smith Mar 30 '15 at 17:33
  • No reason to be sorry at all. If you rather loop through a list of latitudes and longitudes, you can check the code. for latlong in positions where positions = [[60,0],[70,0],[56,7]] etc. Now it gives a new frame for each position sadly. – bjornasm Mar 30 '15 at 17:56
  • 1
    I've edited to replace plt.cla, which removes everything from the axis, with pts.remove() which removes just the last unixtime points. This works with dummy test data. – Ed Smith Mar 30 '15 at 18:30
1

Even though I've gotten an adequate answer over, I found that it was a hassle to get the plot exactly like I wanted, and still animate it - so I did it utilizing some linux tools to make a movie of snapshots instead. This is what I did, for future reference and for others having the same problem:

Timelapse animation, the lazy way

I simply made a plot over all geographical positions for every hour for the whole timespan. This can be done down to every minute, second etc:

    con = lite.connect('database/SAISREAL.db')
    with con:
        cur = con.cursor()
        i = 0
        for k in range(0,137*24): #This is the timespan - every hour for 137 days
            map = Basemap(projection='merc', lat_0=59.45, lon_0=10.5,
            resolution = 'c', area_thresh = 1000,
            llcrnrlon=-30, llcrnrlat=50,
            urcrnrlon=40, urcrnrlat=80)
            map.drawcoastlines()
            map.fillcontinents(color='#27ae60')
            start = 0+k*60*60 #This is again the timespan
            end = 0+(k+1)*60*60

Ok - so now I've established the timespan which I will query data from, as well as drawn the map overlay

            cur.execute('SELECT distinct userid, latitude, longitude FROM geodata WHERE unixtime > {start} AND unixtime < {end}'.format(start = start, end = end))
            data = cur.fetchall()
            if len(data)>0: #Simply check if there is data available
                i = i+1
                filename = ''
                if i<10:
                    filename = '0000'+str(i)
                elif i<100:
                    filename = '000'+str(i)
                elif i<1000:
                    filename = '00'+str(i)
                else:
                    filename = '0'+str(i)
                f,y,x = zip(*data)
                x,y = map(x,y)

The whole filename thing is used later, when I convert the images into a movie - its important that they are named sequentially where everyone has the same number of digits.

                plt.title( str(datetime.datetime.fromtimestamp(int(end)).strftime('%Y-%m-%d')) + ' kl '+str(datetime.datetime.fromtimestamp(int(end)).strftime('%H')))
                plt.scatter(x,y, s=8, alpha=0.7, color="#2980b9", edgecolors='none')

Here I just plotted the info with a timestamp as title.

                plt.savefig('Fisheriesplot/fishplot/'+str(filename)+'.png', format='png')
                plt.clf()

And then, saving the picture. This gives some 3000 .png images - this can obviously be done with other file formats.

Before I either make them to a GIF or a movie, I want to remove the transparent background - to make them appear nicer (less colour shifting between frames)

mkdir batch
for file in *.png ; do convert "${file}" -background black -alpha remove -flatten -alpha off "batch/${file}" ; done
cd batch

If the goal is to make a gif - skip the rest and do this: convert -delay 10 -loop 0 *.png animaion.gif

Option1: Make a movie out of .png

ffmpeg -y -f image2 -framerate 20 -i %05d.png -vcodec png -b 8000k a20k.avi

Just do this is in the folder. Set bitrakte and framerate as you want it- notice that this movie can be quite big.

Option2: Convert images to other format, then make movie

mogrify -format jpg *.png

This is done in terminal in the same folder as the pictures. Then I want to move all the jpg's in their own folder:

mkdir jpgfolder
mv *.jpg jpgfolder

And now, lastly I can make the movie:

cd jpgfolder
ffmpeg -y -f image2 -framerate 4 -i %05d.jpg -vcodec mpeg4 -b 800k a88k.avi

The framerate, which is set to 4 here, should be set to whatever you want. Notice that %05d.jpg says that every file has a leading 0, and has a total of five digits. If its four digits, write 4 etc.

Note that this isn't the most streamlined or smart way to do this, but it is a solution for everyone not wanting to change your code, other than putting it in a loop.

bjornasm
  • 2,211
  • 7
  • 37
  • 62