source: subversion/applications/utils/spreadnik/spreadnik.php @ 29923

Last change on this file since 29923 was 14886, checked in by isortega, 10 years ago

Commit of spreadnik - a tool to help manage big mapnik stylesheets.

  • Property svn:executable set to *
File size: 18.4 KB
Line 
1#!/usr/bin/php5
2<?php
3
4/**
5
6 Spreadnik v0.2
7
8The goal of this script (at least at this version) is to translate a .csv spreadsheet into the mangled XML format that is a mapnik stylesheet.
9
10This will hopefully lead to simplifying the task of customizing mapnik styles: instead of tweaking the stylesheet, tweak the spreadsheet, then run this script, then copy-paste the XML into the stylesheet.
11
12
13 ----------------------------------------------------------------------------
14 "THE BEER-WARE LICENSE":
15 <ivan@sanchezortega.es> wrote this file. As long as you retain this notice you
16 can do whatever you want with this stuff. If we meet some day, and you think
17 this stuff is worth it, you can buy me a beer in return.
18 ----------------------------------------------------------------------------
19
20FIXME: the code needs some refactoring. In particular, the n-dimensional array which holds all the casuistic should be replaced by a tree with n depth levels, and intelligently split nodes as needed when adding symbolizer rules.
21
22*/
23
24/// Config: Set this to the directory where the images are stored. If set correctly, this will mean that there is no need to specify the width, height and type of the images for PointSymbolizers.
25$symbol_dir = "/home/ivan/mapnik/symbols/";
26$symbol_entity = "&symbol_dir;";
27
28// $files = array('highways','stations','symbols','points');
29// $files = array('points');
30$files = array('boundaries','boundaries-area','roads','labels');
31
32/// TODO: loop through the different CSV files in the directory (when there are more than one) instead of hard-coding the filenames here.
33
34
35
36foreach($files as $filename)
37{
38        // OK, let's parse the CSVs
39        // $filename = "highways";
40//      $filename = "stations";
41        echo "Processing $filename\n0%...";
42        $csvf = fopen("$filename.csv",'r');
43       
44       
45        // $csv_column_names = $raw_csv[0];
46       
47        $rows = array();
48        $filter_values = array();
49        $filter_count = 0;
50        unset($csv_columns);
51        // FIRST PASS: Get the column names from the first row and parse all possible values of the filters.
52        while($raw_row = fgetcsv($csvf))
53        {
54                if (!isset($csv_columns))       // First column
55                {
56                        $csv_columns = array();
57                       
58                        // How many columns for filters, rendering rules and zoom factors?
59                        foreach($raw_row as $index=>$csv_column)
60                        {
61                                if ($csv_column == 'z0' ||
62                                $csv_column == 'z1' ||
63                                $csv_column == 'z2' ||
64                                $csv_column == 'z3' ||
65                                $csv_column == 'z4' ||
66                                $csv_column == 'z5' ||
67                                $csv_column == 'z6' ||
68                                $csv_column == 'z7' ||
69                                $csv_column == 'z8' ||
70                                $csv_column == 'z9' ||
71                                $csv_column == 'z10' ||
72                                $csv_column == 'z11' ||
73                                $csv_column == 'z12' ||
74                                $csv_column == 'z13' ||
75                                $csv_column == 'z14' ||
76                                $csv_column == 'z15' ||
77                                $csv_column == 'z16' ||
78                                $csv_column == 'z17' ||
79                                $csv_column == 'z18')
80                                {
81                                        $csv_columns[$index]['type'] = 'zoom';
82                                        $csv_columns[$index]['name'] = substr($csv_column,1);   // Strip the "z"
83                                }
84                                elseif ($csv_column == 'pass')
85                                {
86                                        $csv_columns[$index]['type'] = 'pass';
87                                        $csv_columns[$index]['name'] = $csv_column;
88                                }
89                                elseif ($csv_column == 'symbolizer' )   /// TODO: add textsymbolizer, etc etc etc
90                                {
91                                        $csv_columns[$index]['type'] = 'symbolizer';
92                                        $csv_columns[$index]['name'] = $csv_column;
93                                }
94                                elseif ($csv_column)
95                                {
96                                        // We'll assume anything else (but an empty column) is a filter.
97                                        $csv_columns[$index]['type'] = 'filter';
98                                        $csv_columns[$index]['name'] = $csv_column;
99                                        $filter_names[$filter_count++] = $csv_column;
100                                        $filter_values[$csv_column]['default'] = true;
101                                }
102                        }
103        //              print_r($csv_columns);
104                }
105                else    // Not first row
106                {
107                        $filters    = array();
108                        $pass       = null;
109                        $symbolizer = null;
110                        $zooms      = array();
111                        foreach($raw_row as $index=>$cell)
112                        {
113                                if ($csv_columns[$index]['type'] == 'filter')
114                                {
115                                        if (!$cell) $cell = 'all';
116                                        if ($cell == 'no') $cell = '!yes';
117                                        $filters[ $csv_columns[$index]['name'] ] = $cell;
118                                        if ($cell != 'all' && $cell != 'none')
119                                        {
120                                                if (substr($cell,0,1) == '!')
121                                                {
122                                                        $cell = substr($cell,1);
123                                                }
124                                                $values = explode(',',$cell);
125                                                foreach($values as $value)
126                                                {
127                                                        $filter_values[ $csv_columns[$index]['name'] ][ $value ] = true;
128                                                }
129                                        }
130                                }
131                                elseif ($csv_columns[$index]['type'] == 'symbolizer')
132                                {
133                                        if ($cell)
134                                        $symbolizer = $cell;
135                                }
136                                elseif ($csv_columns[$index]['type'] == 'pass')
137                                {
138                                        if ($cell)
139                                        $pass = $cell;
140                                }
141                                elseif ($csv_columns[$index]['type'] == 'zoom')
142                                {
143                                        if ($cell)
144                                        $zooms[ $csv_columns[$index]['name'] ] = $cell;
145                                }
146                        }
147                       
148        //              echo "$filters, $pass, $symbolizer, $zooms";
149                        if ($zooms && $filters && $pass && $symbolizer)
150                        $rows[] = array($filters, $pass, $symbolizer, $zooms);
151                }
152        }
153       
154        $filter_values_count = array();
155        // Count how many different values are in each filter, for later.
156        foreach($filter_values as $filter=>$values)
157        {
158                foreach($values as $value)
159                        $filter_values_count[$filter]++;
160        }
161       
162        echo "20%...";
163       
164//      print_r($filter_values);
165//      print_r($filter_values_count);
166       
167        // print_r($filter_values);
168        // print_r($rows);
169        // die();
170        //
171       
172        // SECOND PASS: fill the n-dimensional array with the symbolizers and zooms (AKA "unfold the filters")
173        $nbox = array();
174       
175        if (!$define_recursive_fill_nbox)
176        {
177                function recursive_fill_nbox($filters,$level,&$array,$pass,$symbolizer,$zooms)
178                {
179                        global $filter_count,$filter_names;
180                        if ($filter_count == $level)
181                        {
182                //              print_r($zooms);
183                //              foreach($filters[$level] as $option)
184                //              {
185                                        foreach($zooms as $zoom=>$symbol_param)
186                                        {
187                                                $array[$zoom][$pass][$symbolizer] = $symbol_param;
188                                        }
189                //              }
190                        }
191                        else
192                        {
193                                foreach($filters[$filter_names[$level]] as $option)
194                                {
195                                        recursive_fill_nbox($filters,$level+1,&$array[$option],$pass,$symbolizer,$zooms);
196                                }
197                        }
198                }
199                $define_recursive_fill_nbox = true;
200        }
201        // Need to build up alternative filter values array for assigning nodes with 'all'. Basically, swap keys with values.
202        $alt_filter_values = array();
203        foreach($filter_values as $filter=>$values)
204        {
205                foreach($values as $value=>$one)
206                        $alt_filter_values[$filter][] = $value;
207        }
208       
209        // print_r($alt_filter_values);
210       
211        foreach($rows as $row)
212        {
213                list($filters, $pass, $symbolizer, $zooms) = $row;
214               
215                foreach ($filters as $filter=>$values_text)
216                {
217                        $negate_values = false;
218                        if (substr($values_text,0,1) == '!')
219                        {
220                                $negate_values = true;
221                                $values_text = substr($values_text,1);
222                        }
223                       
224                        if ($values_text == 'all')
225                                $values = $alt_filter_values[$filter];
226        //                      $values = $filter_values[$filter];
227                        elseif ($values_text == 'none')
228                                $values = array('');
229                        else
230                                $values = explode(',',$values_text);
231                       
232                        if ($negate_values)
233                        {
234                                $values = array_diff($alt_filter_values[$filter], $values);
235        //                      print_r($values);
236                        }
237                       
238                        $choices[$filter] = $values;
239                }
240        //      echo "entering fill\n";print_r($choices);
241        //      print_r($zooms);
242               
243                recursive_fill_nbox($choices,0,&$nbox,$pass,$symbolizer,$zooms);
244               
245        }
246       
247       
248//      print_r($nbox);
249        // die();
250       
251       
252        // THIRD PASS: refactor the array in order to group the complete symbolizers (pass + symbolizer ruleset + zoom set) shared by several bits of the nbox
253       
254        echo "40%...";
255        $rules = array();
256       
257        if (!$define_extract_serialized_rules)
258        {
259                function recursive_extract_serialized_rules($nbox, $level, &$rules, $path)
260                {
261                        global $filter_count;
262                        if ($level==$filter_count)
263                        {
264                                foreach($nbox as $zoom=>$passes)
265                                {
266                //                      echo "$zoom\n";
267                                        foreach($passes as $pass=>$rule)
268                                        {
269                                                $path_copy = $path;
270                                                $item = &$rules[$pass][ serialize($rule) ][$zoom];
271                                                while (!empty($path_copy))
272                                                {
273                                                        $item = &$item[ array_shift($path_copy) ];
274                                                }
275                                        }
276                                }
277                        }
278                        else
279                        {
280                                foreach($nbox as $path_element=>$child_nbox)
281                                {
282                                        $newpath = $path;
283                                        array_push($newpath,$path_element);
284                                        recursive_extract_serialized_rules($child_nbox, $level+1, &$rules, $newpath);
285                //                      echo ".";
286                                }
287                        }
288                       
289                }
290                $define_extract_serialized_rules = true;
291        }
292       
293        recursive_extract_serialized_rules($nbox, 0, $rules, array());
294       
295//      print_r($rules);
296       
297       
298        // die();
299        //
300       
301        // FOURTH PASS: merge the filter options sub-arrays into SQL-like filter text snippets. This can be done recursively from the leafs to the root of the rules array (that is, until the zoom depth is reached).
302       
303        echo "60%...";
304
305        if (!$defined_sql_filter_from_array_filter)
306        {
307                function sql_filter_from_array_filter($filters,$level)
308                {
309                        global $filter_names,$alt_filter_values,$filter_values_count;
310                        if (!is_array($filters))
311                        {
312                                return '';
313                        }
314                        else
315                        {
316                                $name_of_current_filter = $filter_names[$level];
317                                $temp = array();
318                                foreach($filters as $option=>$subfilters)
319                                {
320                //                      echo "$level $name_of_current_filter $option\n";
321                                        $subfiltertext = sql_filter_from_array_filter($subfilters,$level+1);
322                                        $temp[$subfiltertext][] = $option;
323                                }
324                               
325                //              print_r($temp);
326                               
327                                $or_pieces = array();
328                                foreach($temp as $child_sql_chunk=>$level_options)
329                                {
330                //                      echo "" . count($level_options)  . " == " . $filter_values_count[$name_of_current_filter] . "\n" ;
331                                       
332                                        if (count($level_options) == $filter_values_count[$name_of_current_filter])
333                                        {
334                //                              echo "$name_of_current_filter yay!!\n";
335                                                return $child_sql_chunk;        // There will be no other child_sql_chunks.
336                                        }
337                                        else
338                                        {
339                                                $or_subpieces = array();
340                //                              var_dump($level_options);
341                                                if (array_search('default',$level_options) !== false)
342                                                {
343//                                                      $old_count = count($level_options); $old_options = implode(',',$level_options);
344                                                        $negate = 'not ';
345                                                        $level_options = array_diff($alt_filter_values[$name_of_current_filter], $level_options);
346                                                }
347                                                else
348                                                {
349                                                        $negate = '';
350                                                }
351                                               
352                //                              echo $name_of_current_filter."\n"; print_r($alt_filter_values[$name_of_current_filter]); print_r($level_options); echo "--\n";
353               
354                                                if (array_search('yes',$level_options) !== false)
355                                                {
356                                                        $level_options[] = 'true';
357                                                        $level_options[] = '1';
358                                                }
359                                               
360                                                foreach($level_options as $level_option)
361                                                {
362//                                                      if ($level_option == 1) {echo "WTF?!!!\n"; print_r($level_options);}
363               
364                                                        // Manage options for "lower than" and "greater than".
365                                                        /// TODO: Manage less-or-equal, more-or-equal, and regexps!
366                                                        if (substr($level_option,0,1)=='>')
367                                                                $or_subpieces[] = "[$name_of_current_filter] > " . (float)substr($level_option,1) ;
368                                                        elseif (substr($level_option,0,1)=='<')
369                                                                $or_subpieces[] = "[$name_of_current_filter] < " . (float)substr($level_option,1) ;
370                                                        else
371                                                                $or_subpieces[] = "[$name_of_current_filter] = '$level_option'";
372                                                }
373                                        }
374                                       
375//                                      if (count($or_subpieces)==0) echo "WTF?! No subpieces! $negate $old_count -> ".count($level_options)." == ".$filter_values_count[$name_of_current_filter]." $name_of_current_filter ($old_options)\n";
376//                                      if (count($or_subpieces)==1 && !$or_subpieces[0]) echo "WTF?! Subpieces empty!\n";
377                                       
378                                        if ($child_sql_chunk == '')
379                                        {
380                                                $or_pieces[] = "\n" . str_repeat(" ",$level) . "$negate(" .  implode(' or ',$or_subpieces) . ")";
381                                        }
382                                        else
383                                        {
384                                       
385                                                $or_pieces[] = "\n" . str_repeat(" ",$level) . "( $negate(" . implode(' or ',$or_subpieces) . ") and ( $child_sql_chunk )\n" . str_repeat(" ",$level) . ")";
386                                        }
387                                       
388                                }
389                                return implode(' or ', $or_pieces);
390                        }
391                }
392                $defined_sql_filter_from_array_filter = true;
393        }
394       
395        // Some numbers for the scale factors that mapnik uses in every zoom level.
396        $scale = 443744272;     // See "map.getScale()" in any Openlayers with a epsg:900913 map at z0.
397        $sqrt2 = sqrt(2);
398        for ($i = 0; $i <= 18; $i++)
399        {
400                $zoom_max_scales[$i] = (int) ($scale * $sqrt2);
401                $zoom_min_scales[$i] = (int) ($scale / $sqrt2);
402                $zoom_scales[$i]     = $scale;
403                $scale /= 2;
404        }
405       
406       
407       
408        foreach($rules as $pass=>$passrules)
409        {
410                foreach($passrules as $rule=>$ruleconditions)
411                {
412                        $filterzooms_for_this_rule = array();
413                       
414                        foreach($ruleconditions as $zoom=>&$filters)
415                        {
416                                $filter_text = sql_filter_from_array_filter($filters,0);
417                                $filterzooms_for_this_rule[$filter_text][] = $zoom;
418                        }
419                        unset($ruleconditions);
420                       
421                        // Group adjacent zoom levels into zoom ranges.
422                        foreach($filterzooms_for_this_rule as $filter_text=>$zooms)
423                        {
424                                sort($zooms);   // Just in case.
425                                $last_zoom = null;
426                                foreach ($zooms as $zoom)
427                                {
428                                        if ($last_zoom === null)
429                                        {
430                                                $last_zoom = $zoom;
431                                                $first_zoom = $zoom;
432                                        }
433                                        else
434                                        {
435                                                if ($last_zoom != $zoom-1)
436                                                {
437                                                        $min = $zoom_min_scales[$zoom];
438                                                        $ruleconditions[$filter_text][] = "$first_zoom-$zoom";
439                                                        $max = $zoom_max_scales[$zoom];
440                                                }
441                                                else
442                                                {
443                                                        $last_zoom = $zoom;
444                                                }
445                                        }
446                                }
447                                $min = $zoom_min_scales[$zoom];
448                                $ruleconditions[$filter_text][] = "$first_zoom-$zoom";
449                        }
450                        $rules[$pass][$rule] = $ruleconditions;
451                       
452                }
453        }
454       
455       
456        echo "80%...";
457//      print_r($rules);
458       
459       
460       
461        // SIXTH PASS: The rules array has the right structure now, so let's transverse it and spit the XML out.
462       
463        /// HACK: ensure that the casing is always executed prior to the fill
464        ksort($rules);
465       
466        // Init XML writer stuff
467        $xml = new xmlwriter();
468        $xml->openMemory();
469        $xml->setIndent(true);
470       
471       
472        // Now, just transverse the array and write the stuff inside XML...
473        foreach($rules as $pass=>$passrules)
474        {
475       
476                $xml->startElement('Style');
477                $xml->writeAttribute('name',"$filename-$pass");
478               
479                foreach($passrules as $serial_symbolizers=>$filters)
480                {
481                        $symbolizer_parameter_set = unserialize($serial_symbolizers);
482                       
483                        // Symbolizer parameter sets need to get grouped into individual symbolizers: PointSymbolizer, LinePatternSymbolizer, PolygonPatternSymbolizer, TextSymbolizer, ShieldSymbolizer, LineSymbolizer, PolygonSymbolizer, BuildingSymbolizer, RasterSymbolizer, and MarkersSymbolizer
484                       
485                        $symbolizers = array();
486                        $valid_symbolizer = false;
487                        foreach($symbolizer_parameter_set as $symbolizer_parameter=>$value)
488                        {
489                                $pieces = explode('.',$symbolizer_parameter);
490                                list($symbolizer_type, $symbolizer_param) = $pieces;
491        //                      sscanf($symbolizer_type,"line%d",$symbolizer_type_count);
492                                preg_match('/[0-9]$/',$symbolizer_type,$temp);
493                                if ($temp[0])
494                                {
495                                        $symbolizer_count = $temp[0];
496                                        $symbolizer_type = str_replace($symbolizer_count,'',$symbolizer_type);
497                                }
498                                else
499                                {
500                                        $symbolizer_count = 1;
501                                }
502                               
503                                // Fix up commas and dots for known problematic symbolizer parameter, and abbreviated parameters.
504                                if ($symbolizer_param == 'stroke-width' || $symbolizer_param == 'stroke-opacity'|| $symbolizer_param == 'size')
505                                        $value = str_replace(',','.',$value);
506                                elseif ($symbolizer_param == 'stroke-dasharray')
507                                        $value = str_replace('.',',',$value);
508//                              elseif ($symbolizer_type == 'point' && $symbolizer_param == 'file')
509//                                      $value = "$symbol_dir$value";
510                                elseif ($symbolizer_type == 'text' && $symbolizer_param == 'face')
511                                        $symbolizer_param = "face_name";
512                               
513                                if ($symbolizer_type == 'line') $symbolizer_type = "LineSymbolizer";
514                                if ($symbolizer_type == 'poly') $symbolizer_type = "PolygonSymbolizer";
515                                if ($symbolizer_type == 'text') $symbolizer_type = "TextSymbolizer";
516                                if ($symbolizer_type == 'point') $symbolizer_type = "PointSymbolizer";
517                               
518                                if (($symbolizer_type=='LineSymbolizer'  && $symbolizer_param=='stroke') ||
519                                ($symbolizer_type=='PolygonSymbolizer'  && $symbolizer_param=='fill')   ||
520                                ($symbolizer_type=='TextSymbolizer'  && $symbolizer_param=='face_name'))
521                                        $valid_symbolizer = true;
522                                if ($symbolizer_type=='PointSymbolizer' && $symbolizer_param=='file')
523                                {
524                                        $info = getimagesize( "$symbol_dir$value" );
525                                        $value = "$symbol_entity$value";
526                                        $symbolizers[$symbolizer_type][$symbolizer_count]['width']  = $info[0];
527                                        $symbolizers[$symbolizer_type][$symbolizer_count]['height'] = $info[1];
528                                        $mimetype = $info['mime']; $mimetype = explode('/',$mimetype);
529                                        $symbolizers[$symbolizer_type][$symbolizer_count]['type'] = $mimetype[1];
530        //                              print_r($info);
531                                        $valid_symbolizer = true;
532                                }
533                               
534                               
535                                $symbolizers[$symbolizer_type][$symbolizer_count][$symbolizer_param] = $value;
536                               
537                                /// TODO: for a PointSymbolizer, get the image info (width/height/format) so these parameters are not needed
538//                              echo "$symbolizer_type, $symbolizer_count, $symbolizer_param, $value\n";
539                               
540        //                      $xml->writeComment("$symbolizer_type, $symbolizer_type_count, $symbolizer_param, $value");
541                        }
542                       
543//                      if (!$valid_symbolizer) echo "No valid symbolizers, skipping rule @ pass $pass\n"; // print_r($filters); print_r($symbolizer_parameter_set);
544                       
545                        if ($valid_symbolizer)
546                        {
547                                foreach($filters as $filter_text=>$zooms)
548                                {
549                                        foreach($zooms as $zoomrange)
550                                        {
551                                               
552                                                $twozooms = explode("-",$zoomrange);
553                                                list($maxzoom,$minzoom) = $twozooms;
554                                                $zoom_comment = $maxzoom - $minzoom ? "z$maxzoom - z$minzoom" : "z$maxzoom";
555               
556                                                $xml->startElement('Rule');
557                                               
558                                                if ($maxzoom != 0)
559                                                {
560                                                        $xml->startElement('MaxScaleDenominator');
561                                                        $xml->text($zoom_max_scales[$maxzoom]);
562                                                        $xml->endElement();
563                                                }
564                                                if ($minzoom != 18)
565                                                {
566                                                        $xml->startElement('MinScaleDenominator');
567                                                        $xml->text($zoom_min_scales[$minzoom]);
568                                                        $xml->endElement();
569                                                }
570                                               
571                                                $xml->writeComment(" $zoom_comment ");
572                                                $xml->startElement('Filter');
573                                                $xml->text($filter_text);
574                                                $xml->endElement();
575                                               
576                //                              $xml->writeComment(var_export($symbolizers,1));
577                                               
578                                                foreach($symbolizers as $symbolizer_type=>$type_symbolizers)
579                                                {
580                                                        foreach($type_symbolizers as $symbolizer)       // Skip the symbolizer count. i.e. line2, line3.
581                                                        {
582                                                                if ($symbolizer_type == 'LineSymbolizer' || $symbolizer_type == 'PolygonSymbolizer')
583                                                                {
584                                                                        $xml->startElement($symbolizer_type);
585                                                                       
586                                                                        foreach($symbolizer as $symbolizer_param=>$value)
587                                                                        {
588                                                                                $xml->startElement('CssParameter');
589                                                                                $xml->writeAttribute('name',$symbolizer_param);
590                                                                                $xml->text($value);
591                                                                                $xml->endElement();
592                                                                        }
593                                                                        $xml->endElement();
594                                                                }
595                                                                elseif ($symbolizer_type == 'TextSymbolizer' || $symbolizer_type == 'PointSymbolizer')
596                                                                {
597                                                                        $xml->startElement($symbolizer_type);
598                                                                       
599                                                                        foreach($symbolizer as $symbolizer_param=>$value)
600                                                                        {
601                                                                                $xml->writeAttribute($symbolizer_param,$value);
602                                                                        }
603                                                                        $xml->endElement();
604                                                                }
605                                                                else
606                                                                        echo "No known symbolizer '$symbolizer_type'\n";
607                                                                /// TODO: Add all other symbolizers
608                                                        }
609                                                }
610                                               
611                                               
612                                                $xml->endElement();
613                                               
614                                               
615                                        }
616                                }
617                        }
618                }
619                $xml->endElement();
620                file_put_contents("$filename-$pass.sty",$xml->outputMemory());
621
622        }
623       
624        echo "100%\n";
625       
626}
627
628
Note: See TracBrowser for help on using the repository browser.