<?PHP

class GoogleMaps extends Plugin
{
    function Register()
    {
        $this->Name = "Google Maps";
        $this->Version = "1.1.0";
        $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" => "2.1.2"
            );
        $this->CfgSetup["ExpTime"] = array(
            "Type" => "Number",
            "MaxVal" => 31556926,
            "Label" => "Cache Expiration Time",
            "Help" => "Specifies how long to cache geocode results, in seconds.");
        $this->CfgSetup["DefaultPointField"] = array(
            "Type" => "MetadataField",
            "FieldTypes" =>
            MetadataSchema::MDFTYPE_PARAGRAPH | MetadataSchema::MDFTYPE_TEXT | MetadataSchema::MDFTYPE_POINT,
            "Label" => "Default field for map locations",
            "Help" =>
            "If your geodata is Latitude/Longitude in a point field or addresses in a text field, ".
            "select that field here.  You may then signal ".
            "GoogleMaps_EVENT_HTML_TAGS_SIMPLE in your HTML and won't need to write any PHP."
            );
        $this->CfgSetup["DefaultSqlQuery"] = array(
            "Type" => "Paragraph",
            "Label" => "SQL to generate map locations",
            "Help" =>
            "An SQL select statement which returns your geodata in columns named: ".
            "Latitude, Longitude, ResourceId, MarkerColor, MarkerLabel, LabelColor. ".
            "Column order is not significant, but column names are. ".
            "Latitude, Longitude, and ResourceId are required, the rest are optional. ".
            "NOTE: if you specify both an SQL query and a metadata field, the metadata ".
            "field will be used."
            );
    }

    function Install()
    {
        $this->Upgrade("X-NO-VERSION-X");
    }

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

        switch ($PreviousVersion)
        {
        case "X-NO-VERSION-X":
        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");
        default:
        }
    }

    function DeclareEvents()
    {
        return array(
            "GoogleMaps_EVENT_HTML_TAGS"
            => ApplicationFramework::EVENTTYPE_DEFAULT,
            "GoogleMaps_EVENT_HTML_TAGS_SIMPLE"
            => 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_HTML_TAGS_SIMPLE" => "GenerateHtmlTagsSimple",
            "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");
    }

    function GenerateHTMLTagsSimple($DefaultLat, $DefaultLon, $DefaultZoom, $InfoDisplayEvent)
    {
        $PointProvider = array("GoogleMaps", "DefaultPointProvider");
        $PointProviderParams = array();
        $DetailProvider = array("GoogleMaps", "DefaultDetailProvider");
        $DetailProviderParams = array();

        $this->GenerateHTMLTags($PointProvider, $PointProviderParams,
                                $DetailProvider, $DetailProviderParams,
                                $DefaultLat, $DefaultLon, $DefaultZoom,
                                $InfoDisplayEvent);
    }

    /**
     * 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)));
    }


    static function DefaultPointProvider($Params)
    {
        global $DB;

        $rc = array();

        $MyPlugin = $GLOBALS["G_PluginManager"]->GetPlugin("GoogleMaps");

        $MetadataField = $MyPlugin->ConfigSetting("DefaultPointField");

        if ($MetadataField != "" )
        {
            $Schema = new MetadataSchema();
            $Field = $Schema->GetField($MetadataField);
            $DBFieldName = $Field->DBFieldName();

            if ($Field->Type() == MetadataSchema::MDFTYPE_POINT)
            {
                # Data is lat/long encoded in a point field
                $DB->Query(
                    "SELECT ResourceId, "
                    ."`".$DBFieldName."X` AS Y,"
                    ."`".$DBFieldName."Y` AS X FROM Resources "
                    ."WHERE "
                    ."`".$DBFieldName."Y` IS NOT NULL AND "
                    ."`".$DBFieldName."X` IS NOT NULL");
                $Points = $DB->FetchRows();

                foreach ($Points as $Point)
                {
                    $rc []= array(
                        $Point["X"],
                        $Point["Y"],
                        $Point["ResourceId"],
                        "8F00FF", "", "000000"
                        );
                }
            }
            else
            {
                # Data is an address which needs to be geocoded:
                $DB->Query(
                    "SELECT ResourceId, `".$DBFieldName."` AS Addr FROM Resources "
                    ."WHERE `".$DBFieldName."` IS NOT NULL");
                $Addresses = $DB->FetchRows();

                foreach ($Addresses as $Address)
                {
                    $GeoData = $MyPlugin->Geocode($Address["Addr"]);
                    if ($GeoData !== NULL)
                    {
                        $rc []= array(
                            $GeoData["Lat"],
                            $GeoData["Lng"],
                            $Address["ResourceId"],
                            "8F00FF","","000000"
                            );
                    }
                }
            }
        }
        else
        {
            $SqlQuery =  $MyPlugin->ConfigSetting("DefaultSqlQuery");

            if ($SqlQuery != NULL && !ContainsDangerousSQL($SqlQuery) )
            {
                $DB->Query($SqlQuery);
                while ($Row = $DB->FetchRow())
                {
                    $rc []= array(
                        $Row["Latitude"],
                        $Row["Longitude"],
                        $Row["ResourceId"],
                        isset($Row["MarkerColor"])? $Row["MarkerColor"] : "8F00FF" ,
                        isset($Row["MarkerLabel"])? $Row["MarkerLabel"] : "",
                        isset($Row["LabelColor"]) ? $Row["LabelColor"]  : "000000"
                        );
                }
            }
        }

        return $rc;
    }

    static function DefaultDetailProvider($ResourceId)
    {
        $Resource = new Resource($ResourceId);

        if ($Resource->Status() != 1)
        {
            print("ERROR: Invalid ResourceId\n");
        }
        else
        {
            print(
                '<h1>'.$Resource->GetMapped("Title").'</h1>'
                .'<p>'.$Resource->GetMapped("Description").'</p>'
                .'<p><a href="index.php?P=FullRecord&amp;ID='.$Resource->Id().'">'
                .'(more information)</a></p>'
                );
        }
    }
}
