Location: PHPKode > projects > jjfmapper > jjfmapper/lib/geoformat_shapefile.php
<?php #-*-Mode: php; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
/*
    jjfMapper, a cartography program for PHP 4.
    Copyright (C) 2004  John J Foerch

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

include_once(JJFM_LIBDIR.'/geoformat.php');


class geoformat_shapefile extends geoformat {
    
    var $reportentries;
    
    var $base_name;
    var $dbf_name;
    var $shx_name;
    var $shp_name;  
    
    var $dbf_content;
    var $shx_content;//note to self: not pural. content is a collective noun.
    var $shp_content;

    var $shph;//file handle
    var $shp_content_seekpt = 0;

    var $dbf_keep_content = True;//always true for now. this will be used in
                                 //future to conserve memory.

    var $type;
    var $supported_types = array (1,3,5);

    var $style;
    var $linestyle;
    var $color;
    var $fillcolor;
    var $weight;
    var $dot;
    var $dotcolor = false;

    //match string or regular expression defining what shapes will be drawn.
    var $drawmatch = false;


    function parse_instructions (&$ops, &$symbols) {
        $ret = array();
        if (isset($ops['STYLE'])) $ret['style'] = $ops['STYLE'];
        if (isset($ops['LINESTYLE'])) $ret['linestyle'] = $ops['LINESTYLE'];
        if (isset($ops['WEIGHT'])) $ret['weight'] = $ops['WEIGHT'];
        if (isset($ops['COLOR'])) $ret['color'] = $ops['COLOR'];
        if (isset($ops['FILE'])) $ret['file'] = expand_symbols ($ops['FILE'],
                                                                $symbols);
        if (isset($ops['DRAW'])) $ret['draw'] = $ops['DRAW'];
        if (isset($ops['DOTCOLOR'])) $ret['dotcolor'] = $ops['DOTCOLOR'];
        if (isset($ops['DOT'])) $ret['dot'] = $ops['DOT'];
        return $ret;
    }

    
    function geoformat_shapefile ($args) {
        if (defined ('DEBUG')) $this->reportentries = new debugreport();
        
        if (! isset($args['file']))
        {
            $this->ok=false;
            $this->error='No filename was provided.';
            return;
        }
        
        $this->base_name = $args['file'];
        if (strtolower (substr ($this->base_name, -4)) == '.shp')
        {
            $this->base_name = substr ($this->base_name, 0, -4);
        }

        $this->dbf_name = $this->base_name .'.dbf';
        $this->shx_name = $this->base_name .'.shx';
        $this->shp_name = $this->base_name .'.shp';
        
        if (! (file_exists ($this->dbf_name) |
               file_exists ($this->shx_name) |
               file_exists ($this->shp_name)))
        {
            $this->ok = false;
            $this->error = 'One or more of the DBF, the SHX, '.
                'or the SHP was not found.';
            return;
        }

        $shxh = fopen($this->shx_name,'rb');
        $this->shx_content = fread($shxh,filesize($this->shx_name));
        fclose($shxh);
        
        $this->type = $this->shapetype();
        if (! in_array ($this->type,$this->supported_types))
        {
            $this->ok = false;
            $this->error = 'Shapefiles of type '.$this->type.
                ' are not yet supported.';
            return;
        }

        $ucstyle = isset ($args['style']) ?
            strtoupper ($args['style']) : 'LINE';
        if ($ucstyle == 'FILL') $this->style = JJFM_POLYGON_FILL;
        elseif ($ucstyle == 'BOTH') $this->style = JJFM_POLYGON_OUTLINE |
                                        JJFM_POLYGON_FILL;
        else $this->style = JJFM_POLYGON_OUTLINE;


        $uclinestyle = isset ($args['linestyle']) ?
            strtoupper ($args['linestyle']) : 'SOLID';
        if ($uclinestyle == 'DASHED') $this->linestyle = JJFM_DASHED_LINE;
        else $this->linestyle = JJFM_SOLID_LINE;

        $col = isset ($args['color']) ? $args['color'] : 'black:white';
        list ($this->color, $this->fillcolor) = explode (':', $col);
        if ($this->color == '') $this->color = 'black';
        if ($this->fillcolor == '' && ($this->style & JJFM_POLYGON_FILL))
        {
            $this->fillcolor = $this->color;
            $this->color = 'black';
        }

        $this->weight = isset($args['weight'])?
            $args['weight'] :
            1;//number of pixels of thickness, requires GD 2.0 or greater
        
        if (isset ($args['dot'])) $this->dot = $args['dot'];
        else $this->dot = 'dot';
        if (isset ($args['dotcolor'])) $this->dotcolor = $args['dotcolor'];

        if (isset ($args['draw'])) $this->drawmatch = $args['draw'];

    }
    
    function draw(&$projection) {
        if ($this->drawmatch !== false)
        {
            $dbfdata=$this->DBF_Stage($this->drawmatch);
        } else {
            $dbfdata=false;
        }
        $indexlist=$this->SHX_Stage($dbfdata);
        
        if(defined ('DEBUG') && count($indexlist) == 0)
        {
            $this->reportentries->add (
                array ('warning' => 'Index stage returned nothing.'));
        }
        $dbfdata = null;//free up this bit of memory
        $this->SHP_Stage($projection,$indexlist);
    }
    
    function DBF_Stage() {
        /* Optional argument 0 is a string or perl style regex to match
         * records in the dbf.  If it begins with / it will be treated as a
         * regex.  Otherwise it will be converted to upper case and matched
         * against the record, which will also be uppercased.
         */
        if (! $this->dbf_content)
        {
            $dbfh = fopen($this->dbf_name,'rb');
            $this->dbf_content = fread($dbfh,filesize($this->dbf_name));
            fclose($dbfh);
        }

        $match='';
        if(func_num_args() > 0)
        {
            $match = func_get_arg(0);
            if (substr ($match,0,1) == '/')
            {
                $use_regex = true;
            }
            else {
                $match = strtoupper ($match);
                $use_regex = false;
            }
        }
        $dbfdata = array();

        $dbf_header = unpack(
            'Cversion/Cday/Cmonth/Cyear/Vnumrecords/vheaderlength/vrecordsize',
            substr($this->dbf_content,0,12));
        
        //NOTE
        //Erik Bachmann notes, concerning the record size element,
        //"+1 (deletion flag)"  Yet adding one throws the program off.
        
        for ($a = 0; $a < $dbf_header['numrecords']; $a++) {
            $record = substr ($this->dbf_content,
                              $dbf_header['headerlength'] +
                              $dbf_header['recordsize'] * $a,
                              $dbf_header['recordsize']);
            if(ord(substr($record,0,1)) == 0x2a) continue;
            
            if($match != '') {
                $flag = false;
                if ($use_regex)
                {
                    if (preg_match ($match, $record) != 0) $flag=true;
                } else {
                    if (strpos (strtoupper ($record), $match)) $flag=true;
                }
                if ($flag == false) continue;
            }
            array_push($dbfdata,($a));
        }

        if (! $this->dbf_keep_content) $this->dbf_content = '';
        
        return $dbfdata;
    }
    
    function SHX_Stage (&$dbfdata) {
        /* $dbfdata is an array of record indices to read.  If it is boolean
         * false, all records will be included.
         */
        $indexlist=array();
        if ($dbfdata === false)
        {
            $max = strlen($this->shx_content) - 8;
            for ($a = 100; $a <= $max; $a += 8) {
                list ($var1,$var2) = array_values (
                    unpack ("N2N", substr($this->shx_content,$a,8)));
                $indexlist[$var1*2] = $var2*2;
            }
        } else {
            foreach ($dbfdata as $dbf_k => $dbf_v) {
                list ($var1,$var2) = array_values (
                    unpack("N2N",substr($this->shx_content, 8*$dbf_v+100, 8)));
                // Convert Offset and Content length from words to bytes
                $indexlist[$var1*2] = $var2*2;
            }
        }
        return $indexlist;
    }
    
    function SHP_Stage (&$projection, &$records) {
        $this->shph = fopen($this->shp_name,'rb');
        switch ($this->type) {
            case 1: $this->parse_point ($projection, $records); break;
            case 3: $this->parse_polyline ($projection, $records); break;
            case 5: $this->parse_polygon ($projection, $records); break;
        }
        fclose($this->shph);
    }

    function get_shp_content ($offset, $length) {
        if (! defined('JJFM_SHAPEFILE_CONSERVE_MEMORY'))
        {
            if (! $this->shp_content)
            {
                $shph = fopen($this->shp_name,'rb');
                $this->shp_content = fread($shph,filesize($this->shp_name));
                fclose($shph);
            }

            return substr($this->shp_content,$offset,$length);
        }

        $top = $this->shp_content_seekpt + strlen($this->shp_content);
        if (! ($offset >= $this->shp_content_seekpt &&
               $offset < $top &&
               $offset + $length <= $top))
        {
            if ($length > JJFM_SHAPEFILE_READ_SIZE) $howmuch = $length;
            else $howmuch = JJFM_SHAPEFILE_READ_SIZE;
            fseek($this->shph,$offset);
            $this->shp_content = fread($this->shph,$howmuch);
            $this->shp_content_seekpt = $offset;
        }
        return substr($this->shp_content,
                      $offset - $this->shp_content_seekpt,
                      $length);
    }


    function parse_point (&$projection, &$records) {
        global $unpack_workaround;
        $data = array ();
        foreach ($records as $record_k => $record_v) {
            $ptrec = $this->get_shp_content($record_k + 8, 20);
            $p = 'Vtype/dX/dY';
            $a = $unpack_workaround ($p, $ptrec);
            if ($a['type'] == 1 &&
                $a['X'] >= $projection->xmin &&
                $a['X'] <= $projection->xmax &&
                $a['Y'] >= $projection->ymin &&
                $a['Y'] <= $projection->ymax)
            {
                $data[] = $a['X'];
                $data[] = $a['Y'];
            }
        }
        $geometry = array (
            'point' => array (
                'dot' => $this->dot,
                'dotcolor' => $this->dotcolor,
                'data' => $data
                )
            );
        $projection->draw ($geometry);
    }


    function parse_polyline (&$projection, &$records) {
        global $unpack_workaround;
        foreach ($records as $record_k => $record_v) {
            $mlrec = $this->get_shp_content($record_k + 8,44);
            $p = 'Vtype/d4bounds/Vnparts/Vnpoints';
            $a = $unpack_workaround ($p, $mlrec);
            if (! ($a['type'] == 3 &&
                   RectClip($a['bounds1'],$a['bounds2'],
                            $a['bounds3'],$a['bounds4'],
                            $projection->xmin,$projection->ymin,
                            $projection->xmax,$projection->ymax))) continue;
            $datasize = $a['nparts'] * 4 + $a['npoints'] * 16;
            $p = 'V'.$a['nparts'].'r/d'.($a['npoints']*2).'n';
            $mlrec = $this->get_shp_content($record_k + 52,$datasize);
            $b = $unpack_workaround ($p, $mlrec);
            $points_begin_idx = $record_k + 52 + $a['nparts'] * 4;

            // the shapefile spec says "Parts may or may not be connected to
            // one another."  I interpret this to mean they should not be
            // drawn connected.  If the author of the file intended the parts
            // to be connected, he could do so by making the end point of the
            // last part the same as the start point of the next one.
            // Otherwise, why make them separate parts?

            $data = array();
            $curpart = 1;
            for ($i = 1; $i <= $a['npoints']; ++$i) {
                //add this point to the end of $data
                $xidx = 'n'.(($i - 1) * 2 + 1);
                $yidx = 'n'.(($i - 1) * 2 + 2);
                //print ($i .'   '.$b[$xidx] . '   '. $b[$yidx]."\n");
                $data[] = $b[$xidx]; //x
                $data[] = $b[$yidx]; //y

                //draw the polyline if:
                //    this is the last point overall
                //or: this is the last point in the current part

                $next_point_idx = $i * 16 + $points_begin_idx;
                if ($i == $a['npoints'] ||
                    $curpart < $a['nparts'] &&
                    $next_point_idx == $a['part'.($curpart+1)])
                {
                    $geometry = array (
                        'track' => array (
                            'color' => $this->color,
                            'weight' => $this->weight,
                            'style' => $this->linestyle,
                            'data' => $data
                            )
                        );
                    $projection->draw($geometry);
                    $data = array();
                    ++$curpart;
                }
            }
        }
    }


    function parse_polygon (&$projection, &$records) {
        foreach ($records as $record_k => $record_v) {
            # record_k=Offset record_v=Length
            $ar = unpack('Nctlen',$this->get_shp_content($record_k+4,4));
            $ar['ctlen']*=2; # Words * 2 = Bytes
            $shaperec = $this->get_shp_content($record_k + 8,$ar['ctlen']);

            $rings=$this->ParsePolygonRecord($projection,$shaperec);

            if(is_array($rings))
            {
                foreach ($rings as $points) {
                    $geometry = array (
                        'polygon' => array (
                            'weight' => $this->weight,
                            'style' => $this->style,
                            'color' => $this->color,
                            'fillcolor' => $this->fillcolor,
                            'data' => array($points)
                            )
                        );
                    $projection->draw ($geometry);
                }
            }
        }
    }

    function ParsePolygonRecord (&$projection, &$record) {
        global $unpack_workaround;
        if(strlen($record) == 0) return;
        $upstr = 'VShapetype/d4d/V2V';
        list ($ShapeType,$XMin,$YMin,$XMax,$YMax,$NumParts,$NumPoints) =
            array_values ($unpack_workaround ($upstr, substr ($record,0,44)));
        if(! RectClip($XMin,$YMin,$XMax,$YMax, $projection->xmin,
                      $projection->ymin,$projection->xmax,$projection->ymax))
            return;
        if($ShapeType == 0) return;
        $os=44 + $NumParts * 4;
        if($NumPoints == 0) $this->reportentry('error','no points in polygon');
        $rings=array();//structure that will be returned
        $points=array();
        $startx = $starty = false;
        for($a=0; $a<$NumPoints; ++$a) {
            list($x,$y)=
                array_values($unpack_workaround("d2d",substr($record,$os,16)));
            $points[]=$x;
            $points[]=$y;
            if($startx === false)
            {
                $startx = $x;
                $starty = $y;
            } elseif ($x === $startx && $y === $starty) {
                $startx = $starty = false;
                if ($points[1] !== end($points) &&
                    $points[0] !== prev($points))
                {
                    $points[] = $points[0];
                    $points[] = $points[1];
                }
                $rings[] = $points;
                $points = array();
            }
            $os += 16;
        }
        if ($points)
        {
            if ($points[1] !== end($points) &&
                $points[0] !== prev($points))
            {
                $points[] = $points[0];
                $points[] = $points[1];
            }
            $rings[] = $points;
        }
        return $rings;
    }
    
    function shapetype () {

        $ar = unpack ('V1V',substr ($this->shx_content,32,4));
        return $ar['V'];
                      
    }

    function bounds() {
        /* Optional argument 0 is a string pattern to match in the dbf stage.
         */
        global $unpack_workaround;
        if(func_num_args() > 0)
        {
            $shape = func_get_arg(0);
            $dbfrecords = $this->DBF_Stage($shape);
            $shapelist = $this->SHX_Stage($dbfrecords);
            switch ($this->type) {
                case 1: $r = $this->bounds_point ($shapelist); break;
                case 3: $r = $this->bounds_2d_shape ($shapelist); break;
                case 5: $r = $this->bounds_2d_shape ($shapelist); break;
            }
        } else {
            $r = $unpack_workaround('dxmin/dymin/dxmax/dymax',
                                    substr($this->shx_content,36,32));
        }
        return array($r['ymin'],$r['ymax'],$r['xmin'],$r['xmax']);
    }
    
    function bounds_point (&$shapes) {
        global $unpack_workaround;
        $r = array ('xmin'=>false,'ymin'=>false,'xmax'=>false,'ymax'=>false);
        $this->shph = fopen($this->shp_name,'rb');
        foreach ($shapes as $shape_k => $shape_v) {
            $ptrec = $this->get_shp_content($shape_k + 8, 20);
            $p = 'Vtype/dX/dY';
            $a = $unpack_workaround ($p, $ptrec);
            if ($a['type'] == 1)
            {
                if ($r['xmin'] === false || $a['X'] <= $r['xmin'])
                    $r['xmin'] = $a['X'];
                if ($r['xmax'] === false || $a['X'] >= $r['xmax'])
                    $r['xmax'] = $a['X'];
                if ($r['ymin'] === false || $a['Y'] <= $r['ymin'])
                    $r['ymin'] = $a['Y'];
                if ($r['ymax'] === false || $a['Y'] >= $r['ymax'])
                    $r['ymax'] = $a['Y'];
            }
        }
        fclose($this->shph);
        return $r;
    }

    function bounds_2d_shape (&$shapes) {
        global $unpack_workaround;
        $r = array ('xmin'=>false,'ymin'=>false,'xmax'=>false,'ymax'=>false);
        $this->shph = fopen($this->shp_name,'rb');
        foreach ($shapes as $shape_k => $shape_v) {
            $b = $unpack_workaround('d4d',
                                    $this->get_shp_content($shape_k + 12,32));
            if($r['xmin'] === false || $b['d1'] < $r['xmin'])
               $r['xmin'] = $b['d1'];
            if($r['ymin'] === false || $b['d2'] < $r['ymin'])
                $r['ymin'] = $b['d2'];
            if($r['xmax'] === false || $b['d3'] > $r['xmax'])
                $r['xmax'] = $b['d3'];
            if($r['ymax'] === false || $b['d4'] > $r['ymax'])
                $r['ymax'] = $b['d4'];
        }
        fclose($this->shph);
        return $r;
    }

    function validate() {
        global $unpack_workaround;
        $this->shph = fopen($this->shp_name,'rb');
        $shp_header = $unpack_workaround('N7N/V2V/d8d', $this->get_shp_content(0,100));
        fclose($this->shph);
        $shx_header = $unpack_workaround('N7N/V2V/d8d', substr($this->shx_content,0,100));
        foreach ($shp_header as $hdr_k => $hdr_v) {
            if ($hdr_k == 'N7')
            {
                if ($hdr_v * 2 != filesize($this->shp_name)) return false;
                if ($shx_header[$hdr_k] * 2 != filesize($this->shx_name))
                {
                    return false;
                }
                continue;
            }
            if ($shx_header[$hdr_k] != $hdr_v) return false;
        }
        return true;
    }


    function reportentry ($type, $message) {
        if(defined ('DEBUG'))
        {
            $this->reportentries->add(array($type => $message));
        }
    }




    /* Regarding this commented DBF_Stage function.
       This version of DBF_Stage makes use of the php
       module dbase.  Yet in the real world, it is
       uncommon for people to have this module built
       into their copy of php.  So to obviate the
       trouble of requiring everyone to install the
       dbase module, I made a new DBF_Stage function
       that parses the dbf file itself.  However,
       I leave the old function in, just in case it
       is needed for troubleshooting.
    */
    /*
    function DBF_Stage() {
        $matchcol=''; $match='';
        if(func_num_args() > 0) { $match = strtoupper(func_get_arg(0)); }
        $dbfdata=array();
        $dbasefile=dbase_open($this->dbf_name,0) or
            die("Unable to open dbf.\n");
        for($a=1; $a<=dbase_numrecords($dbasefile); ++$a) {
            $record=dbase_get_record($dbasefile,$a);
            if($record['deleted'] == 1) { continue; }
            if($match != '') {
                $flag = false;
                if ($matchcol != '') {
                    if ($record[$matchcol] == $match) { $flag = true; }
                }
                else {
                    foreach ($record as $r_k => $r_v) {
                        if (strtoupper(trim($r_v)) == $match) {
                            $flag = true; $matchcol = $r_k;
                        }
                    }
                }
                if ($flag == false) { continue; }
            }
            array_push($dbfdata,$a);
        }
        dbase_close($dbasefile);
        return $dbfdata;
    }
    */





}

?>
Return current item: jjfmapper