nrk.no

Lag din eigen trafikkstasjon, del 2

Kategori: Forskning

Designet til trafikkstasjonen vår

Dette er del to av serien kor vi lager ein trafikkstasjon. Har du ikkje lest del éin, gjer det no.

Forrige gong gjekk vi gjennom det å setje opp IP-kameraet, og sende dataene som det registrerte til ein lokal server.
I den andre og siste delen av serien skal vi fullføre prosjektet.

Kameraet har no fått gjort jobben sin i nokre dagar, og har samla nok data til at vi kan byrje å tenkje på presentasjonslaget.
Det absolutt essensielle er jo å få laga nokre grafar som kan vise statistikk per time per dag, og kanskje ein meir detaljert statistikk om dette er ønskjeleg.

Django

Til presentasjon og lagring av dataene vil eg bruke det ypperlege rammeverket Django. Django er eit kraftig rammeverk basert på språket Python, og har ei svært aktiv og engasjert brukargruppe. Eg byrja sjølv med Django for nokre år sidan, og brukar det dagleg, så det er ganske sjølvsagt at eg også til dette prosjektet ønskjer å basere databearbeidinga på det.

Mottak og lagring av informasjonen

Som eg nemna i forrige del skal MySQL stå for lagring av den informasjonen vi får. Databasevalet er relativt uvesentleg, sidan Django sin ORM (ORM på Wikipedia) abstraherer sjølve SQL-koda over til rein Python-kode. Resultatlista vi kjem til å få er også representert som Python-objekter.

Oppsett

Djangos lagmodell kallast ofte MVC (Model-view-controller), men Django sitt kjerneteam syns at betegnelsen MTV (Model-template-view) er ein betre betegnelse. Kort forklart: Du oppretter ein modell som beskriv korleis dataene dine kjem til å sjå ut, du har eit “view” som hentar opp dataene, og du har ein template (mal) som bestemmer korleis informasjonen skal presenterast. Denne modellen gjer at Django si kodebase er veldig enkel å halde oversikta over, ved å strukturere opp dei logiske bitane av applikasjonen din i forskjellige lag.

Semantikken bak ei Django-side har også fått ei anna betydning i seinare tid. Før var det vanleg å ha ei mappe som fungerte som “prosjektet ditt”, som også innehaldt alle applikasjonane dine (det vi bygg i denne serien kan definerast som ein applikasjon). No er det vanleg å opprette prosjektet eksternt frå applikasjonane dine, slik at applikasjonar kan fungere på tvers av prosjekt. Dette er ikkje viktig informasjon med mindre du faktisk har tenkt å gjere stega i denne serien sjølv. Om du har det (og har lite kjennskap til Django og Python), les djangodokumentasjonen og Dive into Python.

Filstruktur

Du kan godt gjennomføre dette prosjektet heime, så lenge du har Python, Django, memcached og MySQL installert på maskina. For å opprette applikasjonstrukturen, gjer følgande frå kommandolinja di i ei mappe som er på PYTHONPATH:

henriklied$ mkdir traffic
henriklied$ cd traffic
henriklied$ touch __init__.py models.py views.py urls.py utils.py admin.py
henriklied$ mkdir templates
henriklied$ cd templates
henriklied$ touch archive_base.html base.html detail_day.html vehicle_archive.html vehicle_archive_month.html vehicle_archive_year.html

Du har no oppretta applikasjonstrukturen vår.

Modellen

Vi må setje opp ein modell som spesifiserer kva for slags data vi skal lagre. Sidan IP-kameraet ikkje gjer anna enn å fortelje oss at “ein bil har køyrd forbi”, er tidspunktet det einaste vi treng å registrere i modellen vår.
Om du har bakgrunn i PHP, er du kanskje vant til å lagre tidspunkt i timestamps. I Python er det vanleg å bruke eit datetime-objekt for å lagre dato/tid. Modellen vår ser slik ut:

from django.db import models
import datetime

class Vehicle(models.Model):
	when = models.DateTimeField()
	
	def save(self):
		self.when = datetime.datetime.now()
		super(Vehicle, self).save()

For å ta det steg for steg gjer vi her følgjande (dette er rett og slett ikkje så veldig viktig…):

  • – Importerer ein modul med navnet models frå foreldremodulen django.db,
    som gjer at vi kan opprette eit nytt child-objekt som skal halde dataene våre

  • – Importerer den innebygde Python-modulen datetime, som gjer at vi kan spesifisere datoer og tider
  • – Opprettar ein ny modell som heitar “Vehicle”. Denne modellen er ei underklasse av “models.Model”, som spesifiserer litt basisfunksjonalitet. Denne skal lagre informasjon om kva tid ein bil har køyrd forbi
  • – Setter attributten “when” som einaste kolonne i tabellen som denne modellen skal opprette
  • – “def” er det samme som “function” i mange andre språk. Her definerer vi at attributten “when” i modellen skal få verdien til tida akkurat no – kvar gong modellen lagrast.

Når dette er gjort går vi i Django-prosjektmappa vår og køyrer syncdb

Innhenting og gruppering av innhald

Etter ei stund har denne modellen no fått tilsendt ein del innhald. Vi kan då bruke Python i ein interaktiv konsoll for å sjå kva for data som har kome inn:

henriklied$ python manage.py shell
Python 2.5.2 (r252:60911, Jul 31 2008, 17:31:22) 
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from traffic.models import Vehicle
>>> v = Vehicle.objects.all()
>>> v
[<Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>]
>>>

Flott! Vi ser at Python hentar opp fire registreringar til oss – det betyr at det har køyrd fire bilar forbi huset.
…men dette er jo ikkje så stilig. Vi må finne ut litt meir om dataene våre.

Vi kan f.eks. sjå kva tidspunkt dei forskjellige bilane køyrde forbi:

henriklied$ python manage.py shell
Python 2.5.2 (r252:60911, Jul 31 2008, 17:31:22) 
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from traffic.models import Vehicle
>>> v = Vehicle.objects.all()
>>> v
[<Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>]
>>> for car in v:
...    print "%d:%d" % (car.when.hour, car.when.minute)
... 
8:17
8:17
8:17
8:17

Her ser vi at alle bilane køyrde forbi på same minutt. Det er jo ganske forståeleg, sidan åtte om morgonen ofte er litt rushtid.

Presentasjon

Som eg nemnde tidlegare brukar Django MTV-modellen. Vi har sett på korleis vi lagar ein modell og spør denne etter data – no må vi sjå korleis vi kan få sendt dette innhaldet til ein HTML-mal. Til dette skal vi innom to ting: views og urls.

Views

Eit view spesifiserer kva for slags data vi skal sende til ein mal. Dette kan vere ein tekststreng, eit tal, ei liste, og mange andre ting. I vårt tilfelle er det tre ting som er mest aktuelt, og det er Django Querysets, hashtables og lister.

Eit view spesifiserast akkurat på samme måte som ein Python-funksjon (“def()”), men det krevjer at første argumentet i funksjonen er eit request-objekt. Vi skal no lage eit view som sender talet over alle bilar som har køyrd forbi huset for dagen i dag (linjer som byrjar med # er kommentarer):

from django.shortcuts import render_to_response
from traffic.models import Vehicle
import datetime

def traffic_for_today(request):
	# Få datoen for i dag
	today = datetime.date.today()
	# Sorter ut alle biler som har kjørt forbi i dag, ved hjelp av variabelen `today` som spesifisert ovenfor.
	vehicles = Vehicles.objects.filter(when__year=today.year, when__month=today.month, when__day=today.day).count()
	
	# Send dataene i viewet til templaten traffic/cars_for_today.html
	return render_to_response('traffic/detail_day.html', {'today': today, 'vehicles': vehicles,})

No har vi definert eit view som skal sende informasjon til malen vår. Men korleis skal Django vite at vi vil sjå denne informasjonen når vi skriv inn URLen http://minside.no/trafikk/? Her kjem vi inn på noko som heiter URLconf.

URLs

Viss du har satt saman nettstader før, er du sikkert kjend med mod_rewrite. Apache mod_rewrite tek ein innkomande forespørsel, og sjekkar det opp mot ei liste over moglege regulære uttrykk forespørselen kan stemme med. I staden for å abstrahere dette til webserveren, spesifiserer du dette i ei fil som heiter urls.py i applikasjonen din. Slik ser vår ut:

from django.conf.urls.defaults import *
from traffic import views as traffic_views

urlpatterns = patterns('traffic.views',
    url(r'^$',
        view=traffic_views.traffic_for_today,
        name='traffic_for_today'),
)

Så lenge du no i prosjektmappa di (mappa som blant anna inneheld prosjektinnstillingane) og legg til (r'^traffic/', include('traffic.urls')), i base-url-fila di, skal adressa http://minside.no/traffic/ sendast til viewet traffic_for_today.

Malen

Om du har gjort alt riktig hittil, skal http://minside.no/traffic/ vise ei blank side. Dette kjem berre av at vi enno ikkje har definert i templaten korleis den skal vise informasjonen den har tilgjengeleg.

Vi legg til følgjande i “traffic/detail_day.html”:

<p> Antall biler i dag: {{ vehicles }}</p>

Viss vi no går på http://minside.no/traffic/ vil vi sjå følgjande:

Slik viser Django eksempelmalen vår
Slik viser Django eksempelmalen vår

Men dette er fortsatt litt kjedeleg. No hoppar vi nokre steg framover, og får eit skikkeleg design på applikasjonen vår. Etter ei lita stund i Photoshop kom eg opp med følgjande:

Designet til trafikkstasjonen vår.
Designet til trafikkstasjonen vår. Teksten er på engelsk av den enkle grunn at eg også har planer om å skrive om dette på min personlege blogg.

Det vi må sjå på no er korleis vi kan vise antal biler som køyrer forbi i timen. Ei framstilling i form av ein graf er heilt klart det mest aktuelle. Eg såg på fleire løysinger, blant anna Plotkit, matplotlib og Google Charts, men endte til slutt opp med Google Visualization, sidan det gjev deg både ein god interaktiv graf, samt har ein veldig enkel plug-and-play kodebase.

Men det går ikkje an å mate rådata frå Djangos ORM inn i API-en til Google Visualization, så vi må køyre dataene våre gjennom nokre filter først. Vi legg til følgjande innhald i traffic/utils.py:


import datetime
from traffic.models import Vehicle	
from django.utils.dateformat import format
	

def grouper(qs, pub_date_field='pub_date', date_format="F Y"):
    """
    This method regroups a dictionary after a certain common attribute.
    In this case, that attribute is a datetime object from a Django Queryset.
    We use the django.utils.dateformat.format method to format out output.
    """
    output = []
    for obj in qs:
        grouper = getattr(obj, pub_date_field)
        grouper = format(grouper, date_format)
        if output and repr(output[-1]['grouper']) == repr(grouper):
            output[-1]['list'].append(obj)
        else:
            output.append({'grouper': grouper, 'list': [obj]})
    return output


def jsDate(dt):
    """
    Get a datetime object, return a Javascript Date() object, 
    but filter out the minutes and seconds. We want even, sortable numbers.
    """
    return "new Date(%s, %s, %s, %s, %s, %s)" % (dt.year, int(dt.month)-1, dt.day, dt.hour, '0', '00')


class Visualization(object):
    def __init__(self, qs):
        self.queryset = qs
        self.total = self.queryset.count()
    
    def render_data(self):
        """ Render the queryset as a dictionary, with a counter."""
        d = {}
        r = grouper(self.queryset, 'when', 'H')
        
        # Add keys to the dictionary grouped by hour
        for i in r:
            d['%s' % str(i['grouper'])] = [str(len(i['list'])), jsDate(i['list'][0].when)]
        return d

Metoden “grouper” tek i mot eit objekt og grupperer det etter ein felles attributt – i dette tilfellet attributten “when” frå modellen Vehicle.

Viss vi no opnar opp Python-konsollen vår, kan vi køyre dataene våre gjennom eit filter:

henriklied$ python manage.py shell
Python 2.5.2 (r252:60911, Jul 31 2008, 17:31:22) 
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from traffic.models import Vehicle
>>> from traffic.utils import Visualization
>>> v = Vehicle.objects.all()
>>> v
[<Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>, <Vehicle: 15.9.2008>]
>>> visual = Visualization(v)
>>> visual.render_data()
{'03': ['1', 'new Date(2008, 8, 15, 3, 0, 00)'], '08': ['7', 'new Date(2008, 8, 15, 8, 0, 00)'], '01': ['1', 'new Date(2008, 8, 15, 1, 0, 00)'], '04': ['1', 'new Date(2008, 8, 15, 4, 0, 00)']}
>>>

Vi ser her at objektlista frå databasen er gjort om til eit heilt anna format. Vi har no ei hashtable (i Python kallast dette for dictionary) med eit key->value pair, kor nøkkelen til alle instansane er timen i eit datetime-objekt, og verdien er antall biler som har passert i løpet av den timen, samt ein Javascript-representasjon av eit dato-element.

Last ned sjølv

Sidan mange av filene er relativt lange, har eg lagt ut alle filene relatert til prosjektet på nettet. Du kan laste ned eit ZIP-arkiv her (MD5 checksum f900ad7d29f7149ce576393aeabc4b3a): http://media.fourmargins.com/traffic.zip – og alt er «free as in beerspeech» – du kan bruke koden på kva som helst slags måte du måtte ønskje. Kodebasen er relativt godt kommentert, så det skal ikkje vere så vanskeleg å setje seg inn i den. Den inneheld også ein del meir enn kva som er gjennomgått i denne posten. Om det er interesse for fleire programmeringsrelaterte artiklar her på NRKbeta, skriv det gjerne i kommentarane. 🙂

Og, ja, her er det ferdige resultatet: Traffic data for Stordal 😀
(ser best ut i Firefox/Safari/Google Chrome/IE8.)

11 kommentarer

  1. Jesper Haug Karsrud

    Dette ser utrolig bra ut, Henrik!
    Merker at det er en stund siden jeg har sett Django/Python-kode, men det gjør ikke så mye! Likte designet på fremstillingen også, ser umåtelig bra ut 😀

    Svar på denne kommentaren

  2. Stilig prosjekt, det her – absolutt! Forstår koden sånn igrove trekk, men python/Django er tydeligvis ganske ulikt det eg har drive med sjølv hittil.

    Framstillinga er fin den, utenom at den gulfargen godt kunne ha vore..litt mindre skrikande 😛

    Svar på denne kommentaren

  3. Sindre Johansen

    Dette var spennende lesning. Jeg har laget noen små prosjekter i django selv, og etter å ha holdt på med php lenge, var det deilig å slippe å knote på databasenivå. Flere slike programmeringsartikler hadde vært toppers 🙂

    Svar på denne kommentaren

  4. «Free as in beer. Du kan bruke koden til hva du vil.»

    Hva har du røyka nå Henke?

    Var ikke så enkelt å skjønne koden tror jeg hvis man ikke kan Django/Python. Men jeg håper folk fikk med seg noen av konseptene med Django i hvert fall.

    Redigert: Hvor kommer avatarene fra forresten? Gravatar?

    Svar på denne kommentaren

  5. Sebastian Steinmann

    Nais henke.
    Tror folk har litt problemer med å følge deg fordi du utelater noen store ting, som hvordan dataene blir lagret i sql databasen osv.

    Likte den grouper metoden din, jeg ville vel gjort noe idiotisk der eller loka værre 😉

    Svar på denne kommentaren

  6. Henrik Lied (NRK)

    Jon Tingvold: Sjekk Wikipedia ang. Free as in Beer – og du har heilt sikkert rett i at det ikkje er så enkelt å forstå koden for nokon som ikkje har erfaring med Python. Om eg hadde intensjoner om at det burde være det er eg ikkje så sikker på. Har man lyst til å lære Python og Django byrjer man ikkje med eit prosjekt som dette – heller den no standardiserte bloggmotoren eller liknande. Eg veit derimot at det byrjar å bli litt folk som har erfaringer med Django her til lands, så kodebasen var vel meir for den delen av lesarane.

    Tor Løvskogen: Fekk ein telefon tidlegare i dag, så det blir vel forhandlinger om aksjeopsjoner på mandag. 🙂

    Sebastian Steinmann: Du har heilt sikkert rett i at folk flest ikkje forstod korleis det heile gjekk føre seg bak scenene. Og ja – grouper-metoden er frykteleg grei å ha i beltet. 🙂

    Svar på denne kommentaren

  7. Mats Taraldsvik

    Henrik Lied: Det du mener er antakelig «Free as in speech», eller fri programvare på norsk? «Free as in beer» blir jo det omvendte, altså du får programmet gratis, men kildekoden er ikke nødvendigvis åpen.

    Spennende artikkel! Artig å lese litt om programmering og hardware.

    Svar på denne kommentaren

Legg igjen en kommentar til Sindre Johansen Avbryt svar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *. Les vår personvernserklæring for informasjon om hvilke data vi lagrer om deg som kommenterer.