P4A 3 framework: db_navigator helper per estrarre un ramo da un dbtree

Esempio di db_navigatorQuesto helper deriva naturalmente dalla funzione descritta nel precedente post.P4A mette a disposizione un widget chiamato P4A_DB_Navigator che si basa su archivi di tipo albero a liste di adiacenza e fornisce un output grafico della lista dei nodi, con un buon grado di interattività. Esiste, per esempio, un metodo che si chiama getPath() che restituisce in un array, il percorso dalla root al nodo fornito in input attraverso la chiave primaria. Purtroppo però, non esiste nessun metodo per estrarre l’insieme di nodi figli da un certo nodo parentale, così ho pensato di adottare la funzione presentata nel precedente post.

I parametri di input sono:

  • $navigator – l’oggetto da cui discende (implicito)
  • $id – l’identificativo del nodo da cui partire
  • $table – il nome della tabella che rappresenta l’albero
  • $pk – il nome della chiave primaria
  • $recursor – il nome del campo che identifica il nodo parentale

Ecco il codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// File: P4A_db_navigator_getBranch.php
<?php
function P4A_db_navigator_getBranch ($navigator, $params)
{
  list($id, $table, $pk, $recursor) = $params;
  $arr = p4a_db::singleton()->getAll("SELECT * FROM $table");
  $pksArr = array();
  foreach ($arr as $rec) {
    $pksArr[$rec[$pk]]=$rec[$recursor];
  }
  $branchIds = array($id);
  $i=0;
  while ($i<count($branchIds)) {
    $newKeys = array_keys($pksArr,$branchIds[$i]);
    if (!empty($newKeys)) {
      foreach ($newKeys as $newKey){
        array_push($branchIds, $newKey);
      }
    }
    ++$i;
  }
  $res = array();
  foreach ($arr as $child) {
    if (in_array($child[$pk], $branchIds)) {
      $res[] = $child;
    }
  }
  return $res;
}
?>

Come esempio ho costruito una semplicissima maschera nella quale, ogni volta che viene selezionato un nodo, viene mostrato un messaggio con l’array dei nodi figli.

Ecco il codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class dbtree extends P4A_Base_Mask
{
  public function __construct()
  {
    parent::__construct();
    $this->setTitle('Test dbTree');
    //Source
    $this->build("p4a_db_source","dbtree")
      ->setTable("tree")
      ->setPk("id")
      ->load()
      ->firstRow();
    $this->setSource($this->dbtree);
    // db_navigator
    $this->build("p4a_db_navigator","navigator")
      ->setSource($this->dbtree)
      ->setRecursor("parent_id")
      ->setDescription("nome")
      ->setStyleProperty("height","85%")
      ->setStyleProperty("overflow","auto")
      ->collapse(true);
    // Display
    $this->display("sidebar_left",$this->navigator);
    // Intercept action afterMoveRow
    $this->intercept($this->dbtree,"afterMoveRow","showBranch");
  }
	public function showBranch()
  {
    $idCommessa = $this->dbtree->fields->id->getNewValue();
    $arr = $this->navigator->getBranch ($idCommessa, "tree", "id", "parent_id");
    $this->info(print_r($arr,true));
  }
}

L’esempio completo è scaricabile qui

Conclusioni

Lancio l’idea di mettere questa funzione fra i metodi del P4A_db_navigator, sempre che non esca fuori qualche dannato baco :-).

Riferimenti ed approfondimenti:

PHP: estrarre un ramo da un albero a liste di adiacenza senza ricorsione

Esempio di struttura ad albero
Esempio di struttura ad albero
Supponiamo di avere un archivio con una struttura ad albero di tipo a liste di adiacenza. Ogni record sarà necessariamente caratterizzato da un identificativo univoco e da un attributo che serve a riconoscere il proprio genitore. Per esempio, nella figura accanto, l’elemento 4 avrà id = 4, e parentId = 1, l’elemento 9 avrà id = 9, e parentId = 4, e così via. Questo modello offre alcuni vantaggi, che sono principalmente la semplicità della struttura e la velocità negli inserimenti. D’altro canto, risultano piuttosto onerosi processi come l’estrazione o la cancellazione di interi rami. Le query risultano quindi, complesse e spesso ricorsive.

Sebbene l’utilizzo di una struttura di tipo nested tree model ideata da Joe Celko sia quasi sempre preferibile, alle volte è necessario cimentarsi con gli algoritmi per la manipolazione delle liste di adiacenza.

Se si deve salire da una foglia fino alla root, non è troppo difficile, basta risalire, attraverso il parentId, il genitore finché parentId = NULL. Il procedimento naturalmente ricorsivo, per questo motivo spesso si indica l’attributo parentId anche con il nome di ricorsore.

Il gioco si fa duro quando si deve estrarre un ramo a partire da un certo genitore… ma come disse John Belushi: “When the going gets tough, the toughs get going!”. Vediamo come fare evitando di farci del male con stored procedure, query ricorsive o tabelle temporanee.
La soluzione che ho adottato prende spunto dagli algoritmi di attraversamento dei grafi: Breadth-first search e Depth-first search. Questi sono procedimenti non ricorsivi che utilizzano stack di tipo FIFO e LIFO per l’esplorazione dei nodi.

L’operazione di fetching da un archivio che rappresenta l’albero della figura in alto produrrà un’array di questo tipo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$arrTest = array (
                    0 => array ("id"=>1, "parent_id"=>NULL),
                    1 => array ("id"=>2, "parent_id"=>1),
                    2 => array ("id"=>3, "parent_id"=>1),
                    3 => array ("id"=>4, "parent_id"=>1),
                    4 => array ("id"=>5, "parent_id"=>2),
                    5 => array ("id"=>6, "parent_id"=>2),
                    6 => array ("id"=>7, "parent_id"=>3),
                    7 => array ("id"=>8, "parent_id"=>3),
                    8 => array ("id"=>9, "parent_id"=>4),
                    9 => array ("id"=>10, "parent_id"=>4),
                    10 => array ("id"=>11, "parent_id"=>4),
                    11 => array ("id"=>12, "parent_id"=>5),
                    12 => array ("id"=>13, "parent_id"=>5),
                    13 => array ("id"=>14, "parent_id"=>6),
                    14 => array ("id"=>15, "parent_id"=>7),
                    15 => array ("id"=>16, "parent_id"=>7),
                    16 => array ("id"=>17, "parent_id"=>9),
                    17 => array ("id"=>18, "parent_id"=>9),
                    18 => array ("id"=>19, "parent_id"=>10),
                    19 => array ("id"=>20, "parent_id"=>11),
                    20 => array ("id"=>21, "parent_id"=>11)
                  );

Ammettiamo ora di voler estrarre l’intero ramo che ha come radice l’elemento 4. La funzione seguente utilizza un nuovo array che viene costruito utilizzando come chiave gli id e come valori i parent_id. Questo ci permette di utilizzare la funzione PHP: array_keys() per le ricerche dei figli, che è piuttosto efficiente.

Ecco il codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function getBranch ($arr,$pk,$recursor,$idBranch)
{
  $pksArr = array();
  foreach ($arr as $rec) {
    $pksArr[$rec[$pk]]=$rec[$recursor];
  }
  $branchIds = array($idBranch);
  $i=0;
  while ($i<count($branchIds)) {
    $newKeys = array_keys($pksArr,$branchIds[$i]);
    if (!empty($newKeys)) {
      foreach ($newKeys as $newKey){
        array_push($branchIds, $newKey);
      }
    }
    ++$i;
  }
  $res = array();
  foreach ($arr as $child) {
    if (in_array($child[$pk], $branchIds)) {
      $res[] = $child;
    }
  }
  return $res;
}

Certo, il codice di questa funzione non è troppo ottimizzato: utilizza tre array (anche se $pksArr e $branchIds sono monodimensionali e quindi “leggeri”) ed esegue tre cicli. In particolare il ciclo (righe 19-23) è piuttosto lento, ma è necessario se si vuole recuperare tutti gli attributi degli elementi estratti oltre agli indispensabili id già presenti in $branchIds. Dalle prove che ho fatto, per archivi fino a 10-15000 records risulta comunque abbastanza efficiente.

Questo è il risultato dell’estrazione del ramo con radice 4 (si nota che l’andamento è di tipo “breadth-first-search” ricerca in ampiezza, seguendo i colori: rosso-verde-blu-giallo):

Array
(
    [0] => Array
        (
            [id] => 4
            [parent_id] => 1
        )

    [1] => Array
        (
            [id] => 9
            [parent_id] => 4
        )

    [2] => Array
        (
            [id] => 10
            [parent_id] => 4
        )

    [3] => Array
        (
            [id] => 11
            [parent_id] => 4
        )

    [4] => Array
        (
            [id] => 17
            [parent_id] => 9
        )

    [5] => Array
        (
            [id] => 18
            [parent_id] => 9
        )

    [6] => Array
        (
            [id] => 19
            [parent_id] => 10
        )

    [7] => Array
        (
            [id] => 20
            [parent_id] => 11
        )

    [8] => Array
        (
            [id] => 21
            [parent_id] => 11
        )

)
// Tempo di esecuzione: 0.2388 msecs

Conclusioni

Personalmente, ho una certa ritrosia nell’uso di funzioni ricorsive, per questo ho perso del tempo a cercare soluzioni alternative e cicliche, ciò non toglie che, ad esempio per recuperare il percorso di una foglia (risalendo quindi verso la root), la ricorsione sia la soluzione più adatta.

Riferimenti ed approfondimenti:

P4A 3 Framework: helper per la gestione degli errori in saveRow() e deleteRow()

Errore di chiave duplicataHo scritto questi due helper per facilitare la gestione degli errori dovuti a query di aggiornamento e di cancellazione record. Gli helper sono una comodissima feature del framework P4A, si tratta dell’opportunità di scrivere funzioni personalizzate che possono essere raggiunte da qualsiasi maschera dell’applicazione. Questa caratteristica consente quindi un efficiente riutilizzo del codice, con grande vantaggio per la leggibilità dello stesso. Per costruire un helper, richiamabile da qualsiasi maschera, è necessario scrivere un file nella directory libraries, all’interno della propria applicazione salvandolo con un nome a piacere preceduto dal suffisso p4a_mask_. All’interno del file deve essere presente una funzione nominata nello stesso modo. Vediamo un esempio di albero di directory:

P4A
 |
 -- applications
      |
      -- gestione_fatture
          |
          -- objects
          |
          -- uploads
          |
          -- libraries  <-- qui! 

All’interno di questa directory posizioniamo i due file:

  • p4a_mask_savewithoutpain.php
  • p4a_mask_deletebreezily.php

Le due funzioni richiamano i metodi saveRow() e deleteRow() all’interno del costrutto PHP: try ... catch, intercettando gli eventuali errori e fornendo un messaggio di avviso senza interrompere il flusso del programma. Quando si utilizzano query di aggiornamento e cancellazione, è sempre bene fare dei controlli perché se si utilizzano tabelle InnoDB si rischia di rompere l’integrità referenziale, se si utilizzano indici di tipo UNIQUE si rischia la duplicazione, e così via. D’altra parte la ricerca tramite SELECT dei vincoli e/o delle chiavi duplicate è spesso troppo laboriosa, anche se in certi casi necessaria.
NOTA: try ... catch funziona solo con P4A 3.x, in quanto è disponibile a partire da PHP5.

Vediamo il codice di p4a_mask_savewithoutpain.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Funzione: Salva senza soffrire :-)
function P4A_Mask_saveWithoutPain($mask, $params)
{    
    list($source) = $params;
    $mask->source = $source; 
	  try{ 
      $mask->source->saveRow();       
    } catch (Exception $e) {  
 
      $res = $e->getMessage();
      preg_match("/\SQLSTATE\[(.+)\]/",$res, $results);
 
      if ($results[1] == '23000') {
        $msg = "Errore: chiave duplicata!";
      }
      else {
        $msg = "Si e' verificato un errore nel salvataggio dei dati";
      }    
      $mask->warning($msg);  
    }
}

Le prime due righe di codice che ho spudoratamente copiato dagli helper di sistema, servono a ricevere gli oggetti: maschera chiamante e data source. Con try si tenta una saveRow(), se fallisce viene valutato il messaggio di errore per lo sbaglio più frequente: chiave duplicata. Negli altri casi viene segnalato un errore generico.

Questo, invece è il codice di p4a_mask_deletebreezily.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Funzione: cancella allegramente... :-)
function P4A_Mask_deleteBreezily($mask, $params)
{
    list($source) = $params;
    $mask->source = $source;
	  try{ 
      $mask->source->deleteRow();       
      } 
    catch (Exception $e) {  
      $res = $e->getMessage();
      $pos = strpos($res, "General error: 1451"); // Errore di rottura integrita'
                                                  // referenziale
      if ($pos==false) {
        $msg = "Si e' verificato un errore nella cancellazione";
      }
      else {
        $msg = "Errore: impossibile cancellare un record correlato";
      }
      $mask->warning($msg);  
    }
}

In questo caso ho valutato solo errori di cancellazione di record che abbiano un vincolo di relazione. Non ho ricercato nella descrizione il codice SQLSTATE perché in SQL lo stesso codice accomuna diversi casi. E’ quindi necessario utilizzare il numero di errore MySQL, che nel caso di rottura dell’integrità referenziale è 1451.

Ecco infine il codice da aggiungere alla nostra ipotetica maschera: (gli helper vengono richiamati omettendo il suffisso p4a_mask_ )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class test_fatture extends p4a_base_mask
{
 public function __construct()
  {
    parent::__construct();
    // DB Source
    $this->build("p4a_db_source", "source")
    // .....
    // codice della maschera...
    // .....
  }
  public function saveRow ()
  {
    $this->saveWithoutPain($this->source);
  }
  public function deleteRow()
  {
    $this->deleteBreezily($this->source);
  }
}

Infine vorrei ricordare gli helper di sistema che, grazie agli autori, ci fanno risparmiare parecchio lavoro!

  • P4A_Field_loadSelectByArray – Imposta un campo di tipo select da un array.
  • P4A_Field_loadSelectByTable – Imposta un campo di tipo select da una tabella.
  • P4A_Frame_anchorTabPane – Ancora un tab pane ad una maschera.
  • P4A_Mask_useToolbar – Imposta una toolbar e la ancora alla topbar.
  • P4A_Mask_setTableAsSource – Imposta un DB_source da una tabella.
  • P4A_Mask_constructSimpleEdit – Costruisce un’intera maschera! (disponibile in P4A 3.2.0)

Conclusioni

Credo che, grazie a strumenti di questo tipo, si può contribuire nello sviluppo Open Source, con poco sforzo e molto vantaggio per tutta la comunità!

Riferimenti ed approfondimenti:


Andrea ha scritto:

Ciao Mario,
complimenti per i gli helper, molto utili :)

Solo un appunto, io eviterei questa linea di codice
$mask->source = $source;
In pratica cosi’ facendo rifai un’assegnazione della variabile source della maschera che non serve: nel tuo esempio quando chiami $this->build(“p4a_db_source”, “source”) hai gia’ popolato la variabile source con un db_source.
Tra l’altro nel tuo esempio non da’ alcun fastidio ma in un caso teorico potrebbe essere un problema perche’ se l’utente inizializzasse la variabile source con un oggetto diverso chiamando quella riga di codice lo andresti a sovrascrivere con risultati del tutto inaspettati (immagina un utente che nella maschera abbia scritto $this->build(‘p4a_table’,’source’)).

Per risolvere il tutto basterebbe cambiare il codice in questa maniera:

try{
$source->saveRow();

Spero di esserti stato utile, a presto
Andrea

ps spero di non aver postato questo commento due volte, la prima volta il server mi ha dato errore… nel caso ignorami ;)
30.12.08 12:04
Mario Spada Author Profile Page ha scritto:

Grazie Andrea per la correzione, hai perfettamente ragione!
30.12.08 19:55

P4A 3 Framework: La nuova release 3.2.0, realizzare una maschera con una riga di codice e tabella editabile

screenshot della grid di P4A 3.2.0
screenshot della grid di P4A 3.2.0
La nuova versione del framework PHP P4A si presenta nel “versioning” con un salto di minor di 2 punti (da 3.0.3 a 3.2.0). Questo indica che le novità sono molte e significative. Forse Fabrizio e Andrea erano in vena di regali natalizi…! Strano, però che non ci sia stato ancora un annuncio ufficiale, io l’ho scoperto quasi per caso. Avevo letto in questo post di Andrea che il lancio era imminente, ma non mi sarei aspettato di trovare nell’area download di sourceforge il pacchetto P4A 3.2.0 già disponibile. Siccome le nuove caratteristiche mi stuzzicavano, l’ho subito provato.

Fra le novità più rilevanti c’è il cambio di tipo di licenza che passa dalla AGPL3 alla LGPL3. Non sono un esperto in materia, e non so dirvi molto. Da quanto ho capito si tratta di una differenza formale che regolamenta l’utilizzo e interazioni fra Librerie, Applicazioni e Lavori combinati.

Passando alle note tecniche, a parte una serie di patch e fix che risolvono alcuni bug, le novità più interessanti sono due. Una è quella del nuovo widget P4A_grid. Si tratta in pratica di una tabella editabile, molto utile per modificare singoli campi senza dover per forza utilizzare il fieldset e la toolbar.
La P4A_grid eredita dalla P4A_table tutti i metodi e le proprietà, in aggiunta presenta i seguenti:

  • void autoSave ()
  • void getRows ($num_page, $rows)
  • void preChange ([$params = null])
  • void saveData ($obj, $params)

Non ho avuto tempo di provare tutto, ma ecco un piccolo test con una tabella di prova per un’ipotetica gestione fatture:

1
2
3
4
5
6
7
  // grid
  $this->build("p4a_grid", "grid")
  ->setWidth(600)
  ->setSource($this->source)
  ->setVisibleCols(array("numero", "descrizione", "cliente"))
  ->showNavigationBar()
  ->autosave();

Il risultato è quello della immagine in alto, chiaramente il campo “lookup” non è editabile, ma il risultato è comunque fantastico. Se poi ci fosse la possibilità di riconoscere automaticamente i campi boolean e renderizzarli con una ckeckbox, e le “lookup” in select sarebbe ancora meglio! (dal post di Andrea sembra che la cosa sia già in cantiere…) Se avesse a disposizione anche un metodo per aggiungere record e cancellarli, sarebbe davvero perfetta, benché con un poco di codice e l’utilizzo dei metodi newRow() e deleteRow() questo dovrebbe essere già possibile.

Dulcis in fundo una piccola chicca, che trovo fantastica!
La possibilità di generare un’intera maschera con tutte le funzionalità per interagire con una tabella, scrivendo due sole righe di codice:

1
2
3
4
5
6
7
8
class test_clienti extends P4A_Base_Mask
{
	public function __construct()
	{
		parent::__construct();
		$this->constructSimpleEdit("clienti");
	}
}

Ma forse due righe sono troppe… ecco come fare scrivendone una sola!

1
2
3
4
5
6
7
8
class test_clienti extends P4A_Simple_Edit_Mask
{
 
	public function __construct()
	{
	 parent::__construct("clienti");
	}	
}

Questo il risultato in uno screenshot:

screenshot della P4A_Simple_Edit_Mask
screenshot della P4A_Simple_Edit_Mask

Nel primo caso utilizzo un nuovo helper (oggetto comodissimo particolarmente per il riutilizzo del codice) che si chiama: P4A_Mask_constructSimpleEdit() e nel secondo una classe che deriva da P4A_Base_Mask

Ci sono numerose altre novità come le migliorie nel comportamento dei CSS, la maggiore aderenza agli standard XHTML ed altri ancora che sono elencati nel file CHANGELOG.

Conclusioni

In attesa della presentazione ufficiale, mi sono permesso di elencare i due aspetti che più mi sono piaciuti! Lunga vita a P4A!

Riferimenti ed approfondimenti:

PHP: una classe per generare un calendario stampabile in PDF

screenshot del calendarioQuesta classe PHP genera un calendario gregoriano in PDF, disponendo ogni mese in una pagina con l’elenco dei giorni feriali e delle festività. E’ possibile scegliere l’anno e cambiare il logo che compare in testa alla pagina.

Si avvicina l’anno nuovo e quasi tutti siamo alla ricerca di calendari. Sebbene molto spesso ci vengono regalati a scopo pubblicitario, una buona parte di questi calendari sono poco utili. Infatti, troppo spesso si tende a privilegiare l’aspetto estetico, a corredo grandi immagini, belle per carità, ma poi i giorni sono scritti piccoli che ci vuole la lente per leggerli. Oppure sono pieni zeppi di informazioni tanto inutili quanto ingombranti, di nomi di santi assolutamente improponibili per gli onomastici dei viventi di questa era, ed ancora, così minimalisti che non sono segnate nemmeno le festività nazionali.

Per questi motivi, ed anche per imparare qualcosa di più sulle funzioni di data e ora in PHP, ho pensato di scrivere una classe in grado di generare il mio calendario da appendere in cucina!

Il codice è abbastanza versatile: genera un’array multidimensionale con i mesi in lettere, i giorni in numero, i giorni della settimana, e un flag che indica se il giorno corrisponde ad una festività oppure no. La classe estende le lbrerie PDF R&OS, in questo modo è sufficiente istanziare una sola classe per generare anche il PDF.

Ecco il codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php 
require_once("class.ezpdf.php");
 
class calendarPDF extends Cezpdf {
 
    var $calArray = array();
    var $yearNum = NULL;
    var $logo = NULL;
    var $easterDay = array();
 
    function buildCalendar($yearYouWant,$logoPath){
      $this->yearNum = empty($yearYouWant) ? intval(date("Y")) : $yearYouWant;
      $this->logo = $logoPath;
      $e = strtotime("$yearYouWant-03-21 +".
                            easter_days($yearYouWant)." days");
      $this->easterDay = array("month"=>intval(date("n",$e)), 
                               "day"=>intval(date("j",$e)));
 
      for ($i=1;$i<=12;$i++) {
        $this->buildMonths($i);        
      }
    }
 
   function buildMonths ($monthNum)
   {
    $giorniSettimana = array("Domenica","Lunedì","Martedì","Mercoledì",
                             "Giovedì","Venerdì","Sabato");
    $mesiAnno = array("GENNAIO","FEBBRAIO","MARZO","APRILE","MAGGIO",
                      "GIUGNO","LUGLIO","AGOSTO","SETTEMBRE","OTTOBRE",
                      "NOVEMBRE","DICEMBRE");                              
    $firstDay = mktime(0,0,0,$monthNum,1,$this->yearNum);
    $daysPerMonth = intval(date("t",$firstDay));
    $monthName = $mesiAnno[$monthNum-1];
    for ($i = 1; $i<=$daysPerMonth; $i++)
    {
      $actualDay = mktime(0,0,0,$monthNum,$i,$this->yearNum);
      $numDay = intval(date("w",$actualDay));
 
      if ($numDay == 0 ) {
        $f = true;
      }
      elseif ($this->isHoliday($monthNum,date("j",$actualDay))) {
          $f = true;
        }
      elseif (($monthNum == $this->easterDay['month']) && 
                (intval(date("j",$actualDay)) == $this->easterDay['day']+1)) {
          $f = true;
      }
      else {
          $f = false;
      }
 
      $this->calArray[$monthName][] = array("day"=>date("d",$actualDay), 
                                      "weekday"=>iconv('UTF-8','CP1252',
                                      $giorniSettimana[$numDay]),
                                      "F"=>$f);                          
    }   
   }
 
   function isHoliday ($monthNumber,$dayNumber)
   {
    $monthNumber = intval($monthNumber);
    $dayNumber = intval($dayNumber);
    $holiday = array(1=>array(1,6), 
                     4=>array(25),
                     5=>array(1),
                     6=>array(2),
                     8=>array(15),
                     11=>array(1),
                     12=>array(8,25,26)                                      
                     );
     $holidayKeys = array_keys($holiday);
     if (in_array($monthNumber,$holidayKeys)){
       if (in_array($dayNumber,$holiday[$monthNumber])) {
        return true;
       }
     }
     return false;
   }
}
?>

Per ottenere il testo in italiano, ho scelto di non utilizzare le funzioni setlocale() e strftime() perché, come viene spiegato in questo post, a seconda del sistema operativo impiegato, cambia la stringa per l’impostazione della lingua, passata come secondo parametro a setlocale(). Per ovviare a questo fastidioso comportamento, ho preferito costruire due array, uno per i nomi dei mesi ed uno per i nomi dei giorni della settimana.
Per la determinazione delle festività ho impostato in un array le date nazionali italiane, ovvero

  • 1 e 6 gennaio
  • 25 aprile
  • 1 maggio
  • 2 giugno
  • 15 agosto
  • 1 novembre
  • 8, 25 e 26 dicembre

Per ottenere, invece, la festività del lunedì dopo Pasqua, è necessario calcolare la data della Pasqua che varia in dipendenza del ciclo lunare e del tipo di calendario (noi utilizziamo il gregoriano, gli ortodossi il giuliano). Chi ha voglia di saperne di più sull’algoritmo di calcolo può fare un’approfondimento qui, chi invece è pigro (come me) può ricorrere alle funzioni PHP easter_date() o easter_days() che restituiscono rispettivamente la data della Paqua in UTS (valida solo fra il 1970 e il 2037) e il numero di giorni dopo il 21 marzo dell’anno in corso.
Poiché easter_date() risulta essere affetta da un bug riportato qui, ho preferito usare easter_days() che riporta sempre date corrette.

Ecco infine il codice per generare il calendario in PDF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
    require_once("calendario_ita.php");
 
    $cal = new calendarPDF('a4','portrait');
    $cal->buildCalendar(2009,"./logo.png");
    $cal->selectFont('./fonts/Helvetica.afm');
    $tmp = array(
      'b'=>'Helvetica-Bold.afm'
      ,'i'=>'Helvetica-Oblique.afm'
      ,'bi'=>'Helvetica-BoldOblique.afm'
      ,'ib'=>'Helvetica-BoldOblique.afm'
      ,'bb'=>'Helvetica-Bold.afm'
    );
    $cal->setFontFamily("Helvetica.afm",$tmp);
    $cal->ezSetMargins(30,20,60,30);
 
    foreach ($cal->calArray as $calMonth => $dati) {
      $cal->ezImage($cal->logo,0,490,"none","left");
      $cal->setColor(0,0,1,0);
      $monthString = "<b>".$calMonth." ".$cal->yearNum."</b>";
      $cal->ezSetY(700);
      $cal->ezText($monthString,18);
      $cal->ezSetDY(-10);
      $cal->setColor(0,0,0);
      foreach ($dati as $singleDay) {
        $toPrint = $singleDay['day']."  ".$singleDay['weekday'];
        if ($singleDay['F']) {
            $toPrint = "<b>".$toPrint."</b>";
            $cal->setColor(1,0,0,0);
        }   
        $y = $cal->ezText($toPrint,14);
        $cal->setColor(0,0,0,0);
        $cal->setLineStyle(1);
        $cal->line(155,$y,540,$y);
        $y = $cal->ezSetDY(-4);
      }
      $cal->ezNewPage();
    }
    $cal->stream();
?>

Se volete solo stampare il calendario cliccate qui, se invece siete interessati anche al codice, in questo pacchetto è disponibile la classe (compresa la libreria R&OS) e il codice per gli esempi.

Conclusioni

Semplice, se nessuno mi regala un calendario… me lo stampo da me!!

Riferimenti ed approfondimenti:

PHP: triangolazioni di Delaunay in 2D

Costruzione dei triangoli di Delaunay (fonte: Wikipedia)Questa particolare tecnica di Geometria computazionale, che prende il nome dal russo Boris Nikolaevich Delone, francesizzato in Delaunay, consente di definire una griglia di triangoli in una superficie in 2D o in 3D, dato un insieme di punti P.
La griglia viene costruita in modo che, per ogni circonferenza circoscritta ad un triangolo, nessun punto di P (oltre a quelli che formano il triangolo stesso) giace all’interno della circonferenza. Questa condizione viene anche detta di cironferenza libera.

La libreria di funzioni, che propongo è la traduzione “letterale” in PHP delle sole funzioni per superfici in 2D, scritte in codice Visual Basic 6 da Franco Languasco, disponibili qui. Il pacchetto di Languasco è davvero molto ben fatto e contempla anche le funzioni per le triangolazioni in 3D, nonchè la visualizzazione grafica del risultato e la possibilità di ruotare i 3 assi. Il codice iniziale risale in realtà alle librerie GEOMPACK3 di Barry Joe originariamente scritte nel 1991 in linguaggio Fortran.

E’ bene precisare che avendo fatto una traduzione “letterale”, il mio codice PHP non risulta molto elegante, e soprattutto non sfrutta appieno le caratteristiche del linguaggio, sebbene già dai primi test, sembra che le prestazioni siano soddisfacenti!

Esistono diversi algoritmi per le triangolazioni di Delaunay, i più importanti sono:

  • Incrementale con complessità O(N2)
  • Dividi et impera con complessità O(NlogN)
  • Convex Hull con complessità O(NlogN)

L’algoritmo incrementale è quello utilizzato in questa libreria, sebbene non sia chiaramente il più efficiente, va più che bene per un insieme non troppo grande di punti.

Funzionamento:

L’algoritmo incrementale descritto da Dani Lischinski in questo documento, inizia con la costruzione di un primo triangolo, largo a sufficienza per contenere tutti i punti dati. I punti vengono aggiunti uno alla volta verificando che la condizione di circonferenza libera venga rispettata.
La prima operazione è quella di connettere i vertici del triangolo su cui giace il nuovo punto con il punto stesso.

algoritmo incrementale passo uno

Si vengono così a formare 3 nuovi triangoli. A questo punto i triangoli vengono verificati per la condizione di circonferenza libera.

algoritmo incrementale passo due

Se è rispettata, si procede con un nuovo punto altrimenti viene eseguita un’operazione che prende il nome di edge flip.

algoritmo incrementale passo tre

Questa operazione si basa sul concetto che la triangolazione di Delaunay massimizza l’angolo minimo.
Poiché in due triangoli che hanno un lato in comune, la somma degli angoli opposti a tale lato è sempre minore di 180°, sarà sufficiente scambiare le diagonali del quadrilatero formato dai due triangoli come mostrato nella figura sotto.

algoritmo incrementale passo tre

Nel codice della libreria di funzioni, ho volutamente lasciato tutti i commenti originali e ho commentato le istruzioni originali Visual Basic senza cancellarle, per poter controllare meglio eventuali errori di traduzione.
L’intera libreria è visibile qui. Questo, invece, è il codice del file di esempio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php 
require_once("delaunay2D.php");
/*
================== PROVA =======================================================
001:   (4.111   0.668)
002:   (-4.209   -3.961)
003:   (-9.720   5.214)
004:   (4.181   -9.093)
005:   (7.252   5.810)
006:   (9.239   7.429)
007:   (8.991   -2.720)
Risultato:
001:   (2   1   3)
002:   (1   2   4)
003:   (1   4   7)
004:   (3   1   5)
005:   (5   1   7)
006:   (3   5   6)
007:   (6   5   7)
*/
//dati
 
  $labels = array("001","002","003","004","005","006","007");
  $XI =  array(4.111,-4.209,-9.720,4.181,7.252,9.239,8.991);
  $YI =  array(0.668,-3.961,5.214,-9.093,5.810,7.429,-2.720); 
 
if (count($XI) <> count($YI)){
  die("le coordinate delle X non sono in numero uguale alle Y");
}
else {
  $NP = count($XI); //N° di punti.  
}
 
echo "Numero punti: ".$NP."<br />";
 
$NT = 0; // N° di triangoli.
 
$IERR = 0;
// Inizializza vettori
$Vcl = array();
$Ind = array();
$TIL = array();
$TNBR = array();
 
    echo ('&lt;pre&gt;');
    for ($I = 1; $I <= $NP; $I++) {
        // Organizza le coordinate dei vertici come
        // richiesto dalle routines di delaunay2D:
        $Vcl[1][$I] = $XI[$I-1]; //Vettore con indice iniziale 1
        $Vcl[2][$I] = $YI[$I-1]; //Vettore con indice iniziale 1
        $Ind[$I] = $I;  // Triangolazione di tutti i vertici contenuti in VCL().
        echo ("Punto $I ".$labels[$I-1]." (".
              $Vcl[1][$I].",".$Vcl[2][$I].")<br />");
    }
    echo ('&lt;/pre&gt;');
 
    // Costruisce le triangolazioni
    $res = DTRIW2($NP, $Vcl, $Ind, $NT, $TIL, $TNBR, $IERR);
 
    echo ("<h2>N. triangoli: ".$NT."</h2>");
 
    if ($IERR == 0) {
      echo ('&lt;pre&gt;');
          for ($I = 1; $I <= $NT; $I++) {
              $Triang[1][$I] = $TIL[1][$I];
              $Triang[2][$I] = $TIL[2][$I];
              $Triang[3][$I] = $TIL[3][$I];
              echo ("Triangolo $I: (".$Triang[1][$I].",".$Triang[2][$I].
                    ",".$Triang[3][$I].")<br />");            
          }
      echo ('&lt;/pre&gt;');        
    }
    else {
      echo "Errore n.: $IERR  - ".zCodiciErrore($IERR)."<br />";
    }
?>

L’array $XI contiene le ascisse dei punti, mentre $YI le ordinate. Tutti i vettori devono avere indice iniziale 1. Le coordinate dei punti vengono passate mediante la matrice $Vcl e il vettore degli indici $Ind alla funzione DTRIW2(). L’output viene fornito passando per riferimento la matrice $TIL così come il vettore per i codici di errore $IERR.

Ho preparato anche questa piccola demo dove è anche possibile visualizzare graficamente i punti su una gif, e al passaggio del mouse, i triangoli che compongono la griglia. Per questo effetto ho utilizzato una map area che viene generata dallo script php e JQuery maphilight uno script che consente di evidenziare le forme geometriche definite nelle map area.

Download

In questo pacchetto è disponibile la libreria e il codice per gli esempi

Ringraziamenti

Ringrazio Franco Languasco per la sua ottima versione Visual Basic 6 senza la quale sarebbe stata dura! Federico Villa, Project Director di Target Tobrukper avermi dato lo spunto a scrivere questo post, e Alessandro Scoscia per le sue dritte geniali!

Conclusioni

Non ho trovato molto sulla generazione di griglie di superfici 2D utilizzando il metodo di Delaunay in PHP, questo potrebbe essere un inizio. Spero, in futuro di migliorare il “porting”, al momento piuttosto grezzo, di questa libreria. Infine, una raccomandazione: non ho potuto testare in modo esaustivo il codice, quindi prima di utilizzarla è bene fare un po’ di prove. Fatemi sapere, in caso di errori!

Riferimenti ed approfondimenti: