Location: PHPKode > projects > jjfmapper > jjfmapper/lib/projection.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
*/


define ('JJFM_POLYGON_OUTLINE',1);
define ('JJFM_POLYGON_FILL',2);
define ('JJFM_SOLID_LINE',4);
define ('JJFM_DASHED_LINE',8);

class jjfProjection {
/*  jjfProjection is a class for converting lat/lon coordinates
to x/y coordinates.  Instantiate it with an associative array
whose keys include: xmin, xmax, ymin, ymax, imwidth, imheight,
and (optionally) padding.  The projection itself is nonstandard.
The international dateline is the edge of the world, so this is
not the projection to use if you need to map say, Alaska.  The
projection is distorted by the value of the cosine of the
latitude in the middle of the map.  Maps that include only a few
degrees of latitude and longitude look best.
*/
    //check variable $ok to find out if the object constructed successfully.
    var $ok=true;
    var $error=false;
    var $mapimage;
    var $palette = array();
    var $params;
    var $xmin,$ymin,$xmax,$ymax,$imwidth,$imheight,
        $xdif,$ydif,$xmid,$ymid,$xrat,$xmul,$ymul,$xadd,$yadd;
    var $reportentries;
    
    function jjfProjection (&$options) {
        $this->params = $options;//store parameters for caching
        if (defined ('DEBUG')) $this->reportentries = new debugreport();
        if (! (isset ($options['xmin']) &&
               isset ($options['ymin']) &&
               isset ($options['xmax']) &&
               isset ($options['ymax']) &&
               isset ($options['imwidth']) &&
               isset ($options['imheight'])))
        {
            if (defined ('DEBUG'))
            {
                $this->reportentries->add(array('error' => 'jjfProjection::'.
                                                'new called without enough '.
                                                'parameters.'));
            }
            $this->ok = false;
            $this->error = 'map dimensions were not passed.';
            return;
        }
        
        if (($options['xmax'] <= $options['xmin'] ||
             $options['ymax'] <= $options['ymin']) &&
            isset($options['padding']) === false)
        {
            if (defined ('DEBUG'))
            {
                $this->reportentries->add(array('error' => 'jjfProjection'.
                                                '::new called with invalid '.
                                                'map dimensions.'));
            }
            $this->ok = false;
            $this->error = 'invalid map dimensions';
            return;
        }
        
        if ($options['imwidth'] == 0 || $options['imheight'] == 0)
        { 
            if (defined ('DEBUG'))
            {
                $this->reportentries->add(array('error' => 'jjfProjection::'.
                                                'new called with invalid '.
                                                'image dimensions.'));
            }
            $this->ok = false;
            $this->error = 'invalid image dimensions';
            return;
        }
        
        $this->ProjectionSetup($options);
    }
    
    function CreateImage() {
        if (defined ('JJFM_USE_GD2'))
        {
            $this->mapimage = imagecreatetruecolor($this->imwidth,
                                                   $this->imheight);
        } else {
            $this->mapimage = imagecreate($this->imwidth,$this->imheight);
        }
        $this->palette['white'] =
            imagecolorallocate($this->mapimage,255,255,255);
        $this->palette['black'] =
            imagecolorallocate($this->mapimage,0,0,0);
        $this->palette['bgcolor'] =
            imagecolorallocate($this->mapimage,255,255,255);
    }
    
    function destroy() {
        imagedestroy($this->mapimage);
    }
    
    function ProjectionSetup (&$options) {
        $this->xmin = $options['xmin'];
        $this->ymin = $options['ymin'];
        $this->xmax = $options['xmax'];
        $this->ymax = $options['ymax'];
        $this->imwidth = $options['imwidth'];
        $this->imheight = $options['imheight'];
        
        $this->xdif = $this->xmax - $options['xmin'];
        $this->ydif = $this->ymax - $options['ymin'];
        $this->xmid = $this->xmin + ($this->xdif / 2);
        $this->ymid = $this->ymin + ($this->ydif / 2);
        $this->xrat = cos($this->ymid * ((atan2(1,1)*4)/180));
        
        if (isset($options['padding']))
        {
            $padtype = $options['padding'][strlen($options['padding']) - 1];
            if ($padtype == '*' || $padtype == '°')
            {
                $this->AddBorderByDegrees(
                    substr($options['padding'],0,
                           strlen($options['padding'])-1));
            } elseif ($padtype =='%') {
                $this->AddBorderByPercent(
                    substr($options['padding'],0,
                           strlen($options['padding'])-1));
            } else {
                $this->AddBorderByPercent($options['padding']);
            }
        } else {
            //calculate final geographic mins and maxes
            //TODO: These four lines cause division by zero
            //TODO: when no adjustment is needed on the
            //TODO: construction arguments.
            $this->xmin = (0 - $this->xadd) / $this->xmul;
            $this->xmax = (($this->imwidth-1) - $this->xadd) / $this->xmul;
            $this->ymin = (0 - $this->yadd) / $this->ymul;
            $this->ymax = (($this->imheight - 1) - $this->yadd) / $this->ymul;
        }
    }
    
    function AddBorderByPercent($percentage) {
        $xadd = $this->xdif * ($percentage / 100);
        $yadd = $this->ydif * ($percentage / 100);
        
        $this->xmin -= $xadd;
        $this->xmax += $xadd;
        $this->ymin -= $yadd;
        $this->ymax += $yadd;
        $this->xdif = $this->xmax - $this->xmin;
        $this->ydif = $this->ymax - $this->ymin;
        $this->xmid = $this->xmin + ($this->xdif / 2);
        $this->ymid = $this->ymin + ($this->ydif / 2);
        $this->xrat = cos($this->ymid * ((atan2(1,1)*4)/180));
        
        $scalebywidthY = $this->imwidth / $this->xdif / $this->xrat;
        $scalebywidthX = $this->imwidth / $this->xdif;
        $scalebyheightY = $this->imheight / $this->ydif;
        $scalebyheightX = $this->imheight / $this->ydif * $this->xrat;
        
        if(($scalebywidthX > $scalebyheightY) &&
           ($this->ydif * $scalebywidthY < $this->imheight))
        {
            $this->xmul = $scalebywidthX;
            $this->ymul = $scalebywidthY;
        } elseif (($scalebywidthX > $scalebyheightY) &&
                  ($this->ydif * $scalebywidthY > $this->imheight)) {
            $this->xmul = $scalebyheightX;
            $this->ymul = $scalebyheightY;
        } elseif (($scalebywidthX < $scalebyheightY) &&
                  ($this->xdif * $scalebyheightX < $this->imwidth)) {
            $this->xmul = $scalebyheightX;
            $this->ymul = $scalebyheightY;  
        } else {
            $this->xmul = $scalebywidthX;
            $this->ymul = $scalebywidthY;
        }
        
        $this->xadd = (0 - $this->xmin) * $this->xmul;
        $this->yadd = (0 - $this->ymin) * $this->ymul;
        
# CENTER THE IMAGE
        $this->xadd += ($this->imwidth - ($this->xdif * $this->xmul))/2;
        $this->yadd += ($this->imheight - ($this->ydif * $this->ymul))/2;
        
# calculate final geographic mins and maxes
        $this->xmin = (0 - $this->xadd) / $this->xmul;
        $this->xmax = (($this->imwidth-1) - $this->xadd) / $this->xmul;
        $this->ymin = (0 - $this->yadd) / $this->ymul;
        $this->ymax = (($this->imheight - 1) - $this->yadd) / $this->ymul;
    }

    function AddBorderByDegrees($degrees) {
        $this->xadd = $degrees;
        $this->yadd = $degrees;
        
        $this->xmin -= $degrees;
        $this->xmax += $degrees;
        $this->ymin -= $degrees;
        $this->ymax += $degrees;
        $this->xdif = $this->xmax - $this->xmin;
        $this->ydif = $this->ymax - $this->ymin;
        $this->xmid = $this->xmin + ($this->xdif / 2);
        $this->ymid = $this->ymin + ($this->ydif / 2);
        $this->xrat = cos($this->ymid * ((atan2(1,1)*4)/180));
        
        $scalebywidthY = $this->imwidth / $this->xdif / $this->xrat;
        $scalebywidthX = $this->imwidth / $this->xdif;
        $scalebyheightY = $this->imheight / $this->ydif;
        $scalebyheightX = $this->imheight / $this->ydif * $this->xrat;
        
        if(($scalebywidthX > $scalebyheightY) &&
           ($this->ydif * $scalebywidthY < $this->imheight))
        {
            $this->xmul = $scalebywidthX;
            $this->ymul = $scalebywidthY;
        } elseif (($scalebywidthX > $scalebyheightY) &&
                  ($this->ydif * $scalebywidthY > $this->imheight)) {
            $this->xmul = $scalebyheightX;
            $this->ymul = $scalebyheightY;
        } elseif (($scalebywidthX < $scalebyheightY) &&
                  ($this->xdif * $scalebyheightX < $this->imwidth)) {
            $this->xmul = $scalebyheightX;
            $this->ymul = $scalebyheightY;  
        } else {
            $this->xmul = $scalebywidthX;
            $this->ymul = $scalebywidthY;
        }
        
        $this->xadd = (0 - $this->xmin) * $this->xmul;
        $this->yadd = (0 - $this->ymin) * $this->ymul;
        
# CENTER THE IMAGE
        $this->xadd += ($this->imwidth - ($this->xdif * $this->xmul))/2;
        $this->yadd += ($this->imheight - ($this->ydif * $this->ymul))/2;
        
# calculate final geographic mins and maxes
        $this->xmin = (0 - $this->xadd) / $this->xmul;
        $this->xmax = (($this->imwidth-1) - $this->xadd) / $this->xmul;
        $this->ymin = (0 - $this->yadd) / $this->ymul;
        $this->ymax = (($this->imheight - 1) - $this->yadd) / $this->ymul;
    }


    function Info() {
        return "xmin: ". $this->xmin ."\n" .
            "xmax: ". $this->xmax ."\n" .
            "ymin: ". $this->ymin ."\n" .
            "ymax: ". $this->ymax ."\n" .
            "imwidth: ". $this->imwidth ."\n" .
            "imheight: ". $this->imheight ."\n";
    }

    function original_params () {
        $s = "projectiontype=jjfProjection\n";
        foreach ($this->params as $p_k => $p_v) {
            $s .= $p_k .'='. $p_v ."\n";
        }
        return $s;
    }

    function TranslateXY($x,$y) {
        $x1=floor($x * $this->xmul + $this->xadd);
        $y1=floor($this->imheight - ($y * $this->ymul + $this->yadd));
        return array($x1,$y1);
    }
    
    function draw (&$struct) {
        //print_r_html ($struct);
        if (! is_array ($struct)) { return; }
        if (count($struct) == 0) { return; }
        foreach ($struct as $geometry_k => $geometry) {
            switch ($geometry_k) {
                case 'polygon':
                    $this->draw_polygons ($geometry);
                    break;
                case 'point':
                    $this->draw_points ($geometry);
                    break;
                case 'track':
                    $this->draw_track ($geometry);
                    break;
            }
        }
        
    }//end function draw()


     /* A polygon structure should have a key 'data' which is an array of
    polygons, each stored in the format requred by imagepolygon().  Other
    allowed keys:

    weight: gd 2.0+, line weight, or thickness

    style: may be 'LINE', 'FILL', or 'BOTH'

    color: an allocated color number. If style is 'BOTH', then the color is
        the fill color and the border color is black.  This will be changed in
        a future version to allow for more customization of the map.
    */
    function draw_polygons (&$geometry) {
        if (! isset ($geometry['data'])) return;
        if (count ($geometry['data']) == 0) return;
        $weight = isset ($geometry['weight']) ? $geometry['weight'] : 1;
        $style = isset ($geometry['style']) ?
            $geometry['style'] : JJFM_POLYGON_OUTLINE;

        $color = isset ($geometry['color']) ? $geometry['color'] : 0;
        $fillcolor = isset ($geometry['fillcolor']) ?
            $geometry['fillcolor'] : $color;
        
        $color = ColorStringToResource ($color, $this->mapimage);
        $fillcolor = ColorStringToResource ($fillcolor, $this->mapimage);

        lineweight ($weight, $this->mapimage);

        foreach ($geometry['data'] as $polygon)
        {
            $verts = count ($polygon) / 2;
            for ($a = 0; $a < $verts; ++$a)
            {
                list ($polygon[$a*2], $polygon[$a*2+1]) =
                    $this->TranslateXY ($polygon[$a*2], $polygon[$a*2+1]);
            }
            if($style & JJFM_POLYGON_FILL)
            {
                imagefilledpolygon($this->mapimage,$polygon,$verts,$fillcolor);
            }
            if($style & JJFM_POLYGON_OUTLINE)
            {
                imagepolygon($this->mapimage,$polygon,$verts,$color);
            }
        }
    }


    /*
        data: an array like ($x, $y, $x1, $y1, $x2, $y2)

        dot: will be tested as a file name, and if the file does not exist, it
            will be passed to GetBuiltInIcon()

        dotcolor: if set, will use ColorIcon() to colorize the dot.  the color
            must be a six digit hex triplet string.  This will change to a
            simple integer in the future.
    */
    function draw_points (&$geometry) {
        if (!isset ($geometry['data'])) return;
        if (count ($geometry['data']) == 0) return;
        $dot_data = false;
        if (isset ($geometry['dot']) &&
            file_exists($geometry['dot']))
        {
            $f = fopen ($geometry['dot'],'rb');
            $dot_data = fread ($f,filesize($geometry['dot']));
            fclose ($f);
        } else {
            $dotname = isset ($geometry['dot']) ? $geometry['dot'] : null;
            $dot_data = GetBuiltInIcon($dotname);
        }
        $dot = imagecreatefromstring ($dot_data);
        imagecolortransparent($dot,imagecolorat($dot,0,0));
        $sx = imagesx ($dot);
        $sy = imagesy ($dot);
        $gx = $sx / 2;
        $gy = $sy / 2;
        if (isset ($geometry['dotcolor']) && $geometry['dotcolor'] != '') {
            ColorIcon ($dot, $geometry['dotcolor']);
        }
        for ($a = 0; $a < (count ($geometry['data']) / 2); ++$a) {
            list ($x,$y) = $this->TranslateXY ($geometry['data'][$a * 2],
                                               $geometry['data'][$a * 2 + 1]);
            imagecopymerge($this->mapimage,$dot,$x-$gx,$y-$gy,0,0,$sx,$sy,99);
        }

        imagedestroy ($dot);
    }


    /*
    data: an array like ($x, $y, $x1, $y1, $x2, $y2)

    trackcolor: if set, will be used for the foreground color
        of a dashed line track.  php4.x does not fully support
        imagedashedline, so a styled line is used instead.
        This makes a true dashed line impossible.

    trackweight: line thickness of the track line. gd 2.0+
        only. default 1.

    style: may be JJFM_SOLID_LINE or JJFM_DASHED_LINE
    */
    function draw_track (&$geometry) {
        global $jjfm_dashedline;
        if (!isset ($geometry['data'])) return;
        if (count ($geometry['data']) == 0) return;
        //draw track
        if (isset ($geometry['weight']))
        {
            lineweight ($geometry['weight'], $this->mapimage);
        }
        if (! isset ($geometry['style']))
        {
            $geometry['style'] = JJFM_DASHED_LINE;
        }
        $color = isset ($geometry['color']) ? $geometry['color'] : 0;
        $color = ColorStringToResource ($color, $this->mapimage);
        $x1 = $y1 = false;
        for ($a = 0; $a < (count ($geometry['data']) / 2); ++$a) {
            list ($x,$y) = $this->TranslateXY ($geometry['data'][$a * 2],
                                               $geometry['data'][$a * 2 + 1]);
            if (! $x1 === false)
            {
                if ($geometry['style'] == JJFM_SOLID_LINE)
                {
                    imageline ($this->mapimage, $x1, $y1, $x, $y, $color);
                } else {
                    $jjfm_dashedline ($this->mapimage, $x1, $y1, $x, $y,
                                      $color,$this->palette['bgcolor']);
                }
            }
            $x1 = $x;
            $y1 = $y;
        }
                
    }


}//JJFProjection

function jjfProjection_parse_info ($s) {
    //this function will be moved to the base class
    //when projections are abstracted to support more
    //than just jjfProjection
    //We will also very likely change this to use xml.
    $ar = explode ("\n", $s);
    $ret = array ();
    foreach ($ar as $v) {
        if ($v == '') continue;
        $j = explode ('=',$v);
        $ret[$j[0]] = $j[1];
    }
    return $ret;
}

?>
Return current item: jjfmapper