Michał Wachowski programista php & webdeveloper

Blog

NewsBox

Własne projekty to fajna rzecz, można robić kiedy się chce, ile się chce i jak się chce. Czasem coś z tego wychodzi. To jeden z takich projektów.

NewsBox, bo o nim mowa, składa się z dwóch podstawowych elementów agregatora i wizualizacji, z czasem dojdzie trzeci ale o nim później.

Agregator

Czyli zbieracz informacji, nie jest wyszukaną konstrukcją. Ot wystarczy pobrać dane ze wskazanego adresu w określonym formacie, sparsować i zapisać w bazie.
Tyle w teorii, w praktyce jest ciut inaczej. Co prawda można tę ideę przenieść bezpośrednio w kod, ale konsekwencje mogą być i na pewno wyjdą wcześniej czy później.

Odczyt ze wszystkich zdefiniowanych kanałów, za jednym zamachem mógłby być zabójczy dla skryptu (i pewnie by był, szczególnie gdy kanałów jest dużo a wśród nich są te z długim czasem odpowiedzi). Więc by uniknąć takich sytuacji zastosowany został prosty i popularny mechanizm. Mianowicie - co ustalony interwał czasu, wywoływane jest polecenie odczytu kanału.
Dopiero podczas wykonywania polecenia, określane jest jaki kanał ma być odczytany. Dane z niego są wpisywane do bazy.

Z racji idei NewsBox'a, który prezentuje ileś najnowszych wpisów z odczytanych kanałów, ilość przechowywanych danych można określić - co prawda nie w sztukach ale w przedziałach czasowych. Przy takim mechanizmie - nie ma sensu trzymać wpisów starszych niż 2 dni.
Tym samym, polecenie odczytu, po umieszczeniu wpisów, usuwa także wpisy starsze niż 2 dni.

Kod odpowiedzialny za to wygląda tak:

public function gather	{
	// odczytanie danych kanaly RSS do odczytu
	// odczytywany bedzie pierwszy lepszy kanal z najmniejsza iloscia odczytow
	$ChannelStorage = new FeedChannelStorage( $this->Container->getComponent('PDOAdapter') );
	$Channel = $ChannelStorage->order('reads', 'asc')->readOne ->execute ;

	// odczytanie kanalu
	// jako wynik zwracany jest obiekt klasy Collection
	$FeedReader = new FeedRSSStorage(new FeedAdapter($Channel->get('address')));
	$Collection = $FeedReader->read ->execute ;

	// uzupelnianie danych dla odczytanych wpisow
	// strip_tags jest do zmiany, powod widac przy szczegolach kazdego z wpisow
	foreach($Collection as $Entry) {
		$Entry->set('id', sha1($Entry->identify ))->set('text', strip_tags($Entry->get('text')))->set('category_id', $Channel->get('category_id'));
	}

	// zapis odczytanych danych z kanalu do bazy
	$FeedEntryStorage = new FeedEntryStorage( $this->Container->getComponent('PDOAdapter') );
	foreach($Collection as $Entry) {
		$FeedEntryStorage->reset ->write($Entry)->execute ;
	}

	// zwiekszanie ilosci odczytow dla danego kanalu
	$Channel->set('reads', $Channel->get('reads')+1);
	$ChannelStorage->write($Channel)->execute ;

	// kasowanie niepotrzebnych wpisow z bazy
	$Collection = $FeedEntryStorage->reset ->condition('pubdate', '<=', date('Y-m-d H:i:s', strtotime('-2 days')))->read ->execute ;
	foreach($Collection as $Entry) {
		$FeedEntryStorage->reset ->delete($Entry)->execute ;
	}

	return new \lib\response\Response200('OK');
}

Prezentacja

Sposób prezentacji jest w sumie najciekawszym elementem całego NewsBox'a. Algorytm użyty do wizualizacji to tzw. "treemap", czyli prezentacja struktury drzewiastej w postaci prostokątów.
Każdy element (liść) struktury opisany jest wagą - w moim przypadku jest to fragment daty. Im wyższa waga tym większy prostokąt.
Algorytmów jest od groma i nazad, najpopularniejsze (wraz z przykładami) można zobaczyć na stronie Bena Shneidermana (wraz z wlepką historyczną). Jednak prezentowane tam algorytmy są średnie. Nie że są do bani i nie działają. Działają, ale rysowana struktura była o złych proporcjach (pionowe prostokąty), albo były zbyt wolne, albo brakowało powtarzalności.

Z pomocą przyszła praca dyplomowa Björna Engdahla z algorytmem przeznaczonym na urządzenia typu PDA o nazwie "Split Treemap".
Zainspirowany tymże algorytmem postanowiłem stworzyć coś na jego podobieństwo. Jak widać - udało się. Algorytm w odróżnieniu od poprzedników nie jest algorytmem sortującym. Przebieg polega na dzieleniu listy elementów na dwie "nierówne" połowy na podstawie wag.

W testach pojawiło się kilka problemów. Z racji, że skrypt ma działać na różnych urządzeniach, o przeróżnych wielkościach ekranu, ilość i wielkość będzie różna w zależności od posiadanej przez klienta rozdzielczości.

Ilość elementów do wyświetlenia można było określić na podstawie roździelczości obszaru roboczego przeglądarki i podzieleniu go przez stałą.
Gorzej było z wielkością. Co prawda za określenie wielkości poszczególnych elementów odpowiadał algorytm, jednak często problem był z dopasowaniem wielkości czcionki tekstu do wielkości prostokąta, przez co tekst wystawał poza element.

Najprostszym sposobem dopasowywania wielkości czcionki było tworzenie elementu wewnątrz prostokąta i operowaniu atrybutem fontSize, tak by wielkość kontenera była mniejsza lub równa prostokątowi w jakim się znajduje.
Jednak w momencie gdy na ekranie ma się znaleźć około stu prostokątów proces skalowania był nazbyt czasochłonny.

Innym podejściem było skalowanie tekstu proporcjonalnie względem pierwszego narysowanego elementu. Problem w tym, że nie można określić jak długą treść zawierać będzie pierwszy element.

Skończyło się na tym, że wielkość czcionki jest obliczana w sposób zbliżony do pierwszego sposobu (skalowanie i sprawdzanie wielkości). Tekst nie jest jednak skalowany a wszystkie obliczenia dokonywane są przy założeniu, że każda litera jest kwadratem (założenie błędne, bo litery są mniejsze i większe... ale większych jest mało, a mniejszch od cholery).

Prócz wielkości prostokąta, o wadze elementu świadczy też jaskrawość koloru. Jak wspomniałem wagą każdego elementu jest czas. Trochę nieprecyzyjnie to ująłem, bo w rzeczywistości jest to ilość sekund od przed-przedwczoraj (2 dni temu). Im mniejsza wartość tym starszy wpis, im większa - tym nowszy.

Więc określanie jaskrawości koloru sprowadza się do określenia proporcji wagi elementu z maksymalną możliwą wagą i przeliczeniu kolorów.

Personalizacja

Trzeci element, który dopiero powstanie - to personalizacja, tak by każdy mógł wybrać jakie kategorie z dostępnych go interesują. Ale to na później... najpierw trzeba poprawić to co już istnieje.

Komentarze

  1. Kamil Kamil

    Nieźle. Implementacja algorytmu wyszła naprawdę dobrze - testowałem w kilku rozdzielczościach i zawsze idealnie zapełnia przestrzeń strony.
    Ps. obsługuj zmianę wielkości rozdzielczości i odświeżaj stronę lub układaj klocki na nowo ;)
    window.addEventListener('resize', resizePage, false);
    window.addEventListener('orientationchange', resizePage, false);

    Odpowiedz
  2. wachowski.michal@

    Pomysł z odświeżaniem przy zmianie rozdzielczości niezgorszy... ale bardziej złożony niż może się to wydawać na pierwszy rzut oka.

    Odpowiedz

Dodaj komentarz