<?PHP

class GoogleMaps extends Plugin
{
    function Register()
    {
        $this->Name = "Google Maps";
        $this->Version = "1.0.5";
        $this->Description = "This plugin adds the ability to insert a Google"
                ." Maps window into a CWIS interface.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array(
            "CWISCore" => "1.9.0"
            );
        $this->CfgSetup["ExpTime"] = array(
            "Type" => "Number",
            "MaxVal" => 31556926,
            "Label" => "Cache Expiration Time",
            "Help" => "Specifies how long to cache geocode results, in seconds.");
    }

    function Install()
    {
        $this->Upgrade(0);
    }

    function Upgrade($PreviousVersion)
    {
        $DB = new Database();

        switch ($PreviousVersion)
        {
        default:

        case "1.0.1":
            $DB->Query("CREATE TABLE GoogleMapsCallbacks ("
                       ."Id VARCHAR(32) UNIQUE, "
                       ."Payload TEXT,"
                       ."LastUsed TIMESTAMP,"
                       ." INDEX (Id))");
        case "1.0.2":
            $DB->Query("CREATE TABLE GoogleMapsGeocodes ("
                       ."Id VARCHAR(32) UNIQUE, "
                       ."Lat DOUBLE, "
                       ."Lng DOUBLE, "
                       ."LastUpdate TIMESTAMP, "
                       ."INDEX (Id))");
            $this->ConfigSetting("ExpTime", 604800);
        case "1.0.3":
            $this->ConfigSetting("DefaultGridSize",10);
            $this->ConfigSetting("DesiredPointCount",100);
            $this->ConfigSetting("MaxIterationCount",10);
        case "1.0.4":
            $DB->Query("ALTER TABLE GoogleMapsCallbacks "
                       ."ADD COLUMN Params TEXT");
        }
    }

    function DeclareEvents()
    {
        return array(
            "GoogleMaps_EVENT_HTML_TAGS"
            => ApplicationFramework::EVENTTYPE_DEFAULT,
            "GoogleMaps_EVENT_STATIC_MAP"
            => ApplicationFramework::EVENTTYPE_DEFAULT,
            "GoogleMaps_EVENT_CHANGE_POINT_PROVIDER"
            => ApplicationFramework::EVENTTYPE_DEFAULT,
            "GoogleMaps_EVENT_GEOCODE"
            => ApplicationFramework::EVENTTYPE_FIRST,
            "GoogleMaps_EVENT_DISTANCE"
            => ApplicationFramework::EVENTTYPE_FIRST,
            "GoogleMaps_EVENT_BEARING"
            => ApplicationFramework::EVENTTYPE_FIRST,
            );
    }

    function HookEvents()
    {
        return array(
            "GoogleMaps_EVENT_HTML_TAGS" => "GenerateHtmlTags",
            "GoogleMaps_EVENT_STATIC_MAP"   => "StaticMap",
            "GoogleMaps_EVENT_CHANGE_POINT_PROVIDER" => "ChangePointProvider",
            "GoogleMaps_EVENT_GEOCODE" => "Geocode",
            "GoogleMaps_EVENT_DISTANCE" => "ComputeDistance",
            "GoogleMaps_EVENT_BEARING" => "ComputeBearing",
            );
    }

    /**
     * Generates the HTML tags to make the gmaps widget.
     *
     * Takes two parameters, a PointProvider and a DetailProvider.
     * Both are the PHP callbacks, the former takes a user-provided
     * array, and is expected to return all of the points with GPS
     * coordinates. The latter should take an id number (usually a
     * ResourceId) and a user-provided array and print a fragment of
     * HTML to display in the info window that pops up over the map
     * marker for that resource.  Anything that can be a php callback
     * is fair game.
     *
     * If you're using functions, they need to be part of the
     * environment when the helper pages for the plugin are loaded.
     * To accomplish this, put them in files called
     * F-(FUNCTION_NAME).html or F-(FUNCTION_NAME).php in your
     * 'interface', 'local/pages' or inside your interface directory.
     *
     * If you're using object methods, the objects will need to be
     * somewhere that the ApplicationFramework's object loading will
     * look, 'local/objects' is likely best.
     */
    function GenerateHTMLTags($PointProvider, $PointProviderParams,
                              $DetailProvider, $DetailProviderParams,
                              $DefaultLat, $DefaultLon, $DefaultZoom,
                              $InfoDisplayEvent)
    {
        # Spit out the html tags required for the map
        print('<div class="GoogleMap"><br/><br/><br/><br/><center>'
                .'<span style="color: #DDDDDD;">[JavaScript Required]'
                .'</span></center></div>');

        foreach(
            array('http://maps.google.com/maps/api/js?sensor=false',
                  './include/jquery.cookie.js',
                  './plugins/GoogleMaps/GoogleMapsExtensions.js')
            as $Script)
        {
            print('<script type="text/javascript"'
                  .' src="'.$Script.'"></script>');
        }

        $PPSerial = serialize($PointProvider);
        $PPParamsSerial = serialize($PointProviderParams);
        $PPHash = md5($PPSerial.$PPParamsSerial);

        $DPSerial = serialize($DetailProvider);
        $DPParamsSerial = serialize($DetailProviderParams);
        $DPHash = md5($DPSerial.$DPParamsSerial);

        $DB = new Database();

        $DB->Query(
            "DELETE FROM GoogleMapsCallbacks WHERE "
            ."NOW() - LastUsed > 86400");

        $DB->Query(
            "INSERT IGNORE INTO GoogleMapsCallbacks "
            ."(Id, Payload, Params, LastUsed) VALUES "
            ."('".$PPHash."','".addslashes($PPSerial)."','".addslashes($PPParamsSerial)."',NOW()),"
            ."('".$DPHash."','".addslashes($DPSerial)."','".addslashes($DPParamsSerial)."',NOW())");

        $MyLocation = 'http://'.$_SERVER['SERVER_NAME'].
            ( strpos($_SERVER['REQUEST_URI'], '.php') ?
              dirname($_SERVER['REQUEST_URI']).'/' :
              $_SERVER['REQUEST_URI']);

        $MapApplication = str_replace(
            array("X-POINT-PROVIDER-X", "X-DETAIL-PROVIDER-X",
                  "X-DEFAULT-LAT-X","X-DEFAULT-LON-X","X-DEFAULT-ZOOM-X",
                  "X-DESIRED-POINT-COUNT-X","X-INFO-DISPLAY-EVENT-X",
                  "X-BASE-URL-X"),
            array($PPHash, $DPHash, $DefaultLat, $DefaultLon, $DefaultZoom,
                  $this->ConfigSetting("DesiredPointCount"),
                  $InfoDisplayEvent, $MyLocation),
            file_get_contents(
                "./plugins/GoogleMaps/GoogleMapsDisplay.js"));

        print('<script type="text/javascript">'."\n");
        print($MapApplication);
        print('</script>'."\n");
    }

    /**
     * Generates javascript code that can be used to change a point
     * provider.  Can be useful with a jQuery .click() to avoid
     * reloading maps.
     */
    function ChangePointProvider($PointProvider, $Params)
    {
        $PPSerial = serialize($PointProvider);
        $PPParamsSerial = serialize($Params);
        $PPHash = md5($PPSerial.$PPParamsSerial);

        $DB = new Database();

        $DB->Query(
            "DELETE FROM GoogleMapsCallbacks WHERE "
            ."NOW() - LastUsed > 86400");

        $DB->Query(
            "INSERT IGNORE INTO GoogleMapsCallbacks "
            ."(Id, Payload, Params, LastUsed) VALUES "
            ."('".$PPHash."','".addslashes($PPSerial)."','".addslashes($PPParamsSerial)."',NOW())");

        print("change_point_provider('".$PPHash."');");
    }

    /**
     * Generates a static map image.
     *
     * This makes use of the Google Static Maps API.
     *
     * Google's docs are here:
     * http://code.google.com/apis/maps/documentation/staticmaps/
     *
     */
    function StaticMap($Lat, $Long, $Width, $Height, $Zoom=14)
    {
        $Url="http://maps.google.com/maps/api/staticmap?maptype=roadmap"
            ."&size=".$Width."x".$Height
            ."&zoom=".$Zoom
            ."&markers=".$Lat.",".$Long
            ."&sensor=false";

        print('<img src="'.$Url.'" alt="Google Map">');
    }

    /**
     * Takes an address, returns a lat/long.
     *
     * Details on Google's Geocoding API are here:
     * http://code.google.com/apis/maps/documentation/geocoding/
     *
     * NB: Geocoding is rate and quantity limited (see the "Limits"
     * section in Google's docs). As of this writing, they allow only
     * 2500 requests per day. Geocode requests sent from servers
     * (rather than via Firefox or IE) appear to be answered slowly,
     * taking about one minute per reply.  Furthermore, google
     * explicitly states that they will block access to their service
     * which is "abusive".
     *
     * To avoid potentials with the rate/quantity issue, this geocoder
     * caches results for up to a week.  If an address is requested
     * which is not in the cache, NULL will be returned and the
     * geocoding request will take place in the background.
     */
    function Geocode($Address)
    {
        $DB = new Database();

        // Clean the cache of entries older than a week.
        $DB->Query("DELETE FROM GoogleMapsGeocodes WHERE "
                   ."LastUpdate - NOW() >=".$this->ConfigSetting("ExpTime"));

        // Then look for the desired address
        $DB->Query("SELECT Lat,Lng FROM GoogleMapsGeocodes "
                   ."WHERE Id='".md5($Address)."'");

        if ($DB->NumRowsSelected()==0)
        {
            // If we can't find it, set up a Geocoding request
            global $AF;

            $AF->QueueUniqueTask(
                array($this,'GeocodeRequest'),
                array($Address)
                );

            return NULL;
        }
        else
        {
            // Otherwise, return it
            $Row = $DB->FetchRow();
            if ($Row["Lat"] !== DB_NOVALUE)
            {
                return array("Lat" => $Row["Lat"],
                             "Lng" => $Row["Lng"]);
            }
            else
            {
                return NULL;
            }
        }
    }

    function GeocodeRequest($Address)
    {
        $Data = file_get_contents(
            "http://maps.google.com/maps/api/geocode/xml?sensor=false&address="
            .urlencode($Address), false,
            stream_context_create(
                array( 'http' => array(
                           'method' => "GET",
                           'header'=>"User-Agent: GoogleMaps/".$this->Version
                           ." CWIS/".CWIS_VERSION." PHP/".PHP_VERSION."\r\n"))
                )
            );

        $ParsedData = simplexml_load_string($Data);
        if ($ParsedData->status == "OK")
        {
            $DB = new Database();
            $DB->Query(
                "INSERT INTO GoogleMapsGeocodes "
                ."(Id,Lat,Lng,LastUpdate) VALUES ("
                ."'".md5($Address)."',"
                .floatval($ParsedData->result->geometry->location->lat).","
                .floatval($ParsedData->result->geometry->location->lng).","
                ."NOW()"
                .")");
        }
        else
        {
            $DB->Query(
                "INSERT INTO GoogleMapsGeocodes "
                ."(Id,Lat,Lng,LastUpdate) VALUES "
                ."('".md5($Address)."',NULL,NULL,NOW())");
        }
    }

    /** Computes the distance in km between two points, assuming a
     * spherical earth.
     */
    function ComputeDistance($LatSrc, $LonSrc,
                             $LatDst, $LonDst)
    {
        // See http://en.wikipedia.org/wiki/Great-circle_distance

        // Convert it all to Radians
        $Ps = deg2rad($LatSrc);
        $Ls = deg2rad($LonSrc);
        $Pf = deg2rad($LatDst);
        $Lf = deg2rad($LonDst);

        // Compute the central angle
        return 6371.01 * atan2(
            sqrt( pow(cos($Pf)*sin($Lf-$Ls),2) +
                  pow(cos($Ps)*sin($Pf) -
                      sin($Ps)*cos($Pf)*cos($Lf-$Ls),2)),
                  sin($Ps)*sin($Pf)+cos($Ps)*cos($Pf)*cos($Lf-$Ls));

    }

    /** Computes the initial angle on a course connecting two points,
     * assuming a spherical earth.
     */
    function ComputeBearing($LatSrc, $LonSrc,
                            $LatDst, $LonDst)
    {
        # See http://mathforum.org/library/drmath/view/55417.html

        // Convert angles to radians
        $Ps = deg2rad($LatSrc);
        $Ls = deg2rad($LonSrc);
        $Pf = deg2rad($LatDst);
        $Lf = deg2rad($LonDst);

        return rad2deg(atan2(sin($Lf-$Ls)*cos($Pf),
                             cos($Ps)*sin($Pf)-sin($Ps)*cos($Pf)*cos($Lf-$Ls)));
    }
}

?>
