poniedziałek, 23 września 2013

Potyczka z generatorem cząsteczek

Opis problemu:

Stworzyć prosty generator cząsteczek. Ma to być okno w którym po kliknięciu myszką we wszystkie kierunki lecą kolorowe kuleczki.

Zastosowane technologie:

Python, Pygame - biblioteka oparta na SDL, z którym miałem do czynienia przy okazji jednego projektu z C++.

Potyczka!

Od pewnego czasu przymierzam się do nauczenia się Python'a. W tym tygodniu z racji braku konkretnych zajęć i poważniejszych obowiązków stwierdziłem, że będzie świetna okazja żeby się w końcu do tego zabrać. Ale jak to bywa z nauką nowego języka, samą teorią nic się nie zdziała, trzeba coś w tym napisać. Padło na prosty generator cząsteczek.
Co do wyboru pygame jako biblioteki to nie bardzo miałem ochotę na robienie jakiegoś projektu backendowego, za to do robienia około-growych rzeczy zawsze znajduje w sobie zacięcie. No i jak już wspominałem miałem bazę, bo kiedyś zrobiłem sidesroller'a w C++ z wykorzystaniem SDL.
Świetne materiały do nauki tej biblioteki można znaleźć TUTAJ. Gorąco polecam, darmową książkę Albert'a Sweigart'a, miło się czyta i wszystko jest prosto wytłumaczone.
Teraz już do samego projektu.
Dla przejrzystości kod podzieliłem na dwa moduły:

  • particles.py - zawiera klasę Particle
  • particlesMaker.py - zawiera main'a, trochę stałych i funkcji
  • Klasa Particle
    class Particle():
      """simple particle class"""
      def __init__(self,x,y,xVel,yVel,color,screenWidth,screenHeight):
        self.x          = x
        self.y          = y
        self.startX     = x
        self.startY     = y
        self.xVel       = xVel
        self.yVel       = yVel
        self.scrWidth   = screenWidth
        self.scrHeight  = screenHeight
        self.color      = color
    
      def update(self):
        self.x += self.xVel
        self.y += self.yVel
        if self.x > self.scrWidth 
    or self.y > self.scrHeight 
    or self.x < 0 or self.y < 0:
          self.x = self.startX
          self.y = self.startY
    
    Jak widać nie jest to jakoś specjalnie rozbudowane. W konstruktorze inicjujemy podstawowe wartości - 'x' i 'y' to nasze współrzędne początkowe, 'xVel' i 'yVel' to wektory przesunięcia reszty już nie będę tłumaczył. Powiem tylko, że 'self.x' i 'self.y' to aktualne położenie cząsteczki, natomiast 'self.startX' i 'self.startY' to jej punkt startowy. Będzie do niego wracać po wypadnięciu z ekranu.
    Co się dzieje w metodzie update? Aktualizujemy pozycję cząsteczki i, jeżeli jest poza ekranem, sprowadzamy ją z powrotem do punktu startowego.
    Jak widać sama cząsteczka nie jest specjalnie skomplikowana - nie używa też żadnych metod z pygame.

    Moduł particlesMaker
    Zaczynamy kilkoma importami i deklaracją potrzebnych zmiennych:
    import pygame,sys,particles,random
    from pygame.locals import *
    from collections import deque
    
    #definitions of global constans
    SCRWIDTH  = 800
    SCRHEIGHT = 600
    MAXPARTICLES = 800
    FPS = 40
    NEWPARTICLES = 2
    MAXXVEL = 4
    MAXYVEL = 4
    
    #colors
    #name        R    G    B
    GRAY     = (100, 100, 100)
    NAVYBLUE = ( 60,  60, 100)
    WHITE    = (255, 255, 255)
    RED      = (255,   0,   0)
    GREEN    = (  0, 255,   0)
    BLUE     = (  0,   0, 255)
    YELLOW   = (255, 255,   0)
    ORANGE   = (255, 128,   0)
    PURPLE   = (255,   0, 255)
    CYAN     = (  0, 255, 255)
    
    ALLCOLORS = [RED, GREEN, GRAY, BLUE, YELLOW, ORANGE, PURPLE, CYAN, NAVYBLUE]
    
    No dobra, w pierwszej linijce importujemy potrzebne nam moduły, w tym nasz moduł particles. W drugiej linijce mamy import wszystkiego z pygame.locals - są tam deklaracje stałych takich jak typy eventów i dzięki temu importowi możemy się do nich odwoływać bezpośrednio, a nie przez pygame.locals.NAZWA_STALEJ.
    W trzeciej linii importuje klasę deque, będącą zgrabną implementacją kolejki (my wykorzystamy ją konkretnie jako FIFO). Będziemy w niej przechowywać nasze cząsteczki.
    Ze stałych wyjaśnię tylko 'MAXPARTICLES' - będzie to liczba cząsteczek do wygenerowania w każdej klatce.
    Dalej mamy definicję kolorów i wrzucenie ich do jednej listy. Kolory w pygame są reprezentowane przez trójkę wartości z zakresu 0-255, odpowiadającą wartością RGB.
    Lecimy dalej.
    def main():
      pygame.init()
      SCREEN = pygame.display.set_mode((SCRWIDTH,SCRHEIGHT))
      pygame.display.set_caption("Particles generator")
      CLOCK = pygame.time.Clock()
      particles = deque([],MAXPARTICLES)
      mouseDown = False
      mousePos = (None,None)
    
      running = True
      while running:
        SCREEN.fill(WHITE)
        #events handling
        for event in pygame.event.get():
          if event.type == QUIT:
            running = False
          elif event.type == KEYDOWN:
            if event.key == K_ESCAPE:
              pause()
          elif event.type == MOUSEBUTTONDOWN:
            mouseDown = True
            mousePos = event.pos
          elif event.type == MOUSEBUTTONUP:
            mouseDown = False
          elif event.type == MOUSEMOTION:
            mousePos = event.pos
    
        #add new particles if mouse button is hold
        if mouseDown:
          generateParticles(particles,mousePos)
        #draw particles
        for part in particles:
          part.update()
          pygame.draw.circle(SCREEN,part.color,(part.x,part.y),2)
    
        pygame.display.flip()
        CLOCK.tick(FPS)
      pygame.quit()
      sys.exit()
    
    main()
    Zaczynamy od odpalenia pygame przez funkcję 'init()'. Bez tego nic nie ruszy.
    Dalej inicjujemy "powierzchnię" do której będziemy rysować. Ogólnie wygląda to tak, że na monitor rzucamy przygotowany wcześniej pojedynczy obraz. Dla przykładu gdy mamy 3 drzewa, gracza i pieska to nie wyświetlamy najpierw tła, potem drzew, potem... Wrzucamy to wszystko na jeden "obrazek" i dopiero ten "obrazek" wyświetlamy na monitorze.
    4 linijka to ustawienie tytułu okna, 5 to inicjacja zegara (wyjaśnię po co za chwilę). Dalej mamy konstrukcję naszej kolejki, pierwszy argument to dane jakie zostaną dodane, chcemy pustą kolejkę więc jest tam pusta lista. Drugi argument to maksymalny rozmiar kolejki, bardzo fajna sprawa, bo myślałem, że będę musiał sprawdzać ręcznie czy się nie przepełniła. A tak gdy przekroczymy dozwoloną ilość cząsteczek to kolejka pozbędzie się kilku "najstarszych" elementów.
    Dalej mamy zmienne sterujące.

    Gra będzie działała w pętli while, z każdym przebiegiem wykonując 3 główne typy zadań:

  • Sprawdzenie i obsługa oczekujących eventów - zdarzeń takich jak naciśnięcie klawisza czy przesunięcie myszki
  • Aktualizacja stanu - logika gry, rzeczy takie jak poruszanie obiektów, wykrywanie kolizji itp.
  • Wyświetlanie aktualnego stanu

  • Zaczynamy od wypełnienia całego naszego ekrany białym tłem - kasujemy w ten sposób to co działo się w poprzedniej klatce. W pętli for obsługujemy zdarzenia. Ich listę zwraca nam pygame.event.get(). Dalej sprawdzamy jaki event nastąpił i przeprowadzamy odpowiednią akcję. Nazwy eventów raczej mówią same za siebie: QUIT - dla przykładu to kliknięcie "krzyżyka" zamykającego okno. Na kliknięcie escape wywołuję pauzę, ale jeszcze nie chciało mi się jej implementować, więc to nic nie robi. Kliknięcie przycisku myszy przełącza flagę mouseDown na true i pobiera pozycję kursora na ekranie. Przy zwolnieniu przycisku myszki przestawiam flagę na false, a przy ruchu aktualizuję pozycję kursora. Nic trudnego.
    Jeżeli przycisk myszki jest wciśnięty wywołuje generator cząsteczek. Opisze jego działanie za chwilę. Na koniec wywołuję metodę update() dla każdej cząsteczki w naszej kolejce. W linii 34 rysuję okrąg odpowiedniego koloru we współrzędnych x i y naszego obiektu. Tak robię dla wszystkich naszych cząsteczek.
    pygame.display.filp() jest funkcją wrzucającą nasz SCREEN na monitor (wcześniej rysowaliśmy do SCREEN).
    No i jest nasz zegar - jego zadaniem jest tutaj zapewnienie, żeby pętla nie wykonała się więcej razy niż 'FPS' w ciągu sekundy. Gdyby tego tu nie było pętla leciałaby tak szybko jak mogła - w przypadku tego programu pewnie i z 200 razy na sekundę - zżerając niepotrzebnie zasoby.
    Po wyjściu z pętli while wywołujemy pygame.quit() aby zamknąć wszystkie moduły i sys.exit(), które kończy działanie aplikacji.

    generateParticles
    Zostało tylko wyjaśnić jedną funkcję pomocniczą.
    def generateParticles(list,pos):
      """generates new particles and maintaining there is not to much of then"""
      for i in range(NEWPARTICLES):
        list.append(particles.Particle(pos[0],pos[1],random.randint(-MAXXVEL,MAXXVEL)
    ,random.randint(-MAXYVEL,MAXYVEL)
    ,ALLCOLORS[random.randint(0,len(ALLCOLORS)-1)],SCRWIDTH,SCRHEIGHT))
    
    Dużo znowu się tutaj nie dzieje. Jako że nasza kolejka sama dba o zachowanie odpowiedniej wielkości, wystarczy że dodamy do niej określoną liczbę cząsteczek. To się dzieje w pętli for i jest w niej wywoływany konstruktor Particle z aktualną pozycją myszki, randomowymi wektorami przesunięć, losowym kolorem i naszymi rozmiarami ekranu. Tyle.

    Małe podsumowanie

    --Przemyślenia - można pominąć--
    Nie jest to specjalnie zaawansowany projekt, w sam raz dla kogoś kto startuje z pythonem i pygame. Ja sam mam zamiar rozwinąć go o kilka dodatkowych typów cząsteczek i jakiś interfejs, w którym użytkownik będzie mógł trochę namieszać ze zmiennymi - szybkością, ilością, typem itd.

    Kod źródłowy jest dostępny w moim repozytorium na gitHub.
    Serdecznie zachęcam do zabawy z kodem!

    sobota, 21 września 2013

    Potyczka z Google Maps 1/2

    Opis problemu:

    Stworzyć stronę z mapą googla na której po zaznaczeniu miejsca lub wybraniu go z pola podpowiedzi rysujemy okrąg o zadanej przez użytkownika średnicy (w kilometrach) a następnie wyświetlamy w odpowiednich miejscach markery z informacjami z bazy danych.

    Zastosowane technologie:

    Google Maps API - javascript, do tego jQuery, php do komunikacji z bazą danych, wykorzystałem JSON-a do podania informacji z PHP do javascriptu. Do tego był jeszcze skrypt perlowy do rozkodowywania danych, ale to inna historia.

    Potyczka!

    Nie jestem jakimś wielkim web designerem, więc nie specjalnie przejmowałem się bajerami na stronie, szablonami ani niczym podobnym. Ważne było wyświetlanie informacji, więc sama mapa zajmuje dobre 3/4 miejsca. Na górze wrzuciłem textfielda do wpisywania adresu do którego podpiąłem autocomplete od googla, oraz formę do wpisywania długości i szerokości geograficznych z palca. Jest tam jeszcze elegancki suwak do wyboru długości promienia okręgu, z jQueryUI.

    No to od początku. Dane które miałem do wyświetlenia były dosyć specyficzne - depesze meteorologiczne SYNOP. Wygląda to to mniej więcej tak:
    AAXX 02019 17061 05/82 /1110 10081 20068 39435 40042 55004 60002 8/000
    Kilka dni wcześniej pisałem do tego skrypt perlowy który rozkodowywał tą depeszę, ale nie będę się w to teraz zagłębiał.
    W bazie mam osobną tabelę na raporty, a osobną tabelę na stację z których raporty są. W stacjach mam ich współrzędne geograficzne także większego problemu nie było.
    Żeby wyświetlić dane z zadanego okręgu stwierdziłem, że najlepiej z bazy wyjąć depesze z kwadratu opisanego na tym okręgu, a później zastosować proste równanie okręgu.
    d=(x-a)2+(y-b)2
    d<r2
    Gdzie środek okręgu jest w punkcie (x,y) a punkt mamy w punkcie (a,b).
    No ale oczywiście zapomniałem, że przecież współrzędne geograficzne nijak się mają do szkolnego układu współrzędnych.
    Jeszcze większy problem pojawił się przy szukaniu kwadratu opartego na okręgu. Jakkolwiek długość geograficzna ma stałą wartość w kilometrach, tak szerokość zmienia się w zależności od długości. Trzeba było kombinować i udało mi się znaleźć TO. Trzeba było to przepisać na php-a i gotowa funkcja do znajdowania długości i szerokości geograficznej punktu w zadanej odległości i kierunku od innego wygląda tak:
    function toRad($arg){
      return $arg*pi()/180;
    }
    function toDeg($arg)
    {
      return $arg*180/pi();
    }
    function getLatLon($side,$dist,$lat,$lng)
    {
      $dist=$dist/6371;
      $side=toRad($side);
    
      $lat1=toRad($lat);
      $lng1=toRad($lng);
    
      $lat2=asin(sin($lat1)*cos($dist)
        +cos($lat1)*sin($dist)*cos($side));
      $lng2=$lng1 + atan2(sin($side)*sin($dist)*cos($lat1)
        ,cos($dist)-sin($lat1)*sin($lat2));
      
      $result = array(toDeg($lat2),toDeg($lng2)); 
      return $result;
    } 

    Teraz po dostaniu współrzędnych punktu wywoływałem getLatLon z różnymi parametrami $side: 0 dla punktu na północ od środka, 90 na wschód itd. Zapytanie do bazy było już proste do stworzenia i nie będę się tu w to wgłębiał. Trzeba tylko napomknąć co z tej bazy wyłowiłem. Miałem id stacji, długość i szerokość geograficzną i treść raportu.
    Potem musiałem to wrzucić do pliku zakodowanego JSON-em. Zdecydowałem się na tablicę w takim formacie:
    $tablica = array(station_ID=>array(array(raporty_danej_stacji),dlugosc,szerokosc))
    Jak widać tablice wielowymiarowe się przydają i muszę przyznać, że w php-ie robi się takie rzeczy na prawdę wygodnie. Ale wychodzi tutaj kolejny problem, może być wiele raportów z tej samej stacji, a ja chciałbym je wyświetlić wszystkie w jednym markerze. Można to załatwić pewnie SQL-em ale ja zdecydowałem się na prosty warunek przy wrzucaniu danych z bazy do tablicy.
      while($row=$statement->fetch()){
        if(array_key_exists($row['STATION_ID'],$resultArray)){
          $resultArray[$row['STATION_ID']]["content"]
            [count($resultArray[$row['STATION_ID']]["content"])]=
            $row['CONTENT'];
        }
        else{
          $resultArray[$row['STATION_ID']]=array(
            "content"=>array(0=>$row['CONTENT']),
            "lat"=>$row['STATION_LAT'],
            "lng"=>$row['STATION_LONG']);
        }
        
      }
    
    Jak widać po ifie mamy wstrętnego jednolinijkowca - zrobiłem to w ten sposób żeby zachować wewnętrzną tablicę z contentem jako pełnoprawną asocjacyjną - było to potrzebne ze względu na kodowanie JSON. Chociaż teraz pewnie zrobiłbym to inaczej bo nie jestem specjalnie z tego zadowolony.
    Ze strony php zostało już tylko zakodowanie i zapisanie pliku. Robi się to w jednej linijce (po otworzeniu pliku oczywiście):
      fwrite($OUT,json_encode($resultArray));
    

    Tyle na dzisiaj. W następnej części przejdziemy do javascript i samych google maps.
    Zapraszam!