NewsBox
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.
Nieźle. Implementacja algorytmu wyszła naprawdę dobrze - testowałem w kilku rozdzielczościach i zawsze idealnie zapełnia przestrzeń strony.
OdpowiedzPs. 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);
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