PHP 从 5
起,新增了关于日出和日落的函数:date_sunrise
、date_sunset
(PHP
5.1.2 起还有 date_sun_info
函数,有兴趣的可以看看),这对于要根据日出日落时间改变网页内容的人来说是一个福音。我由此想到了可以先用
JS 获取当前所在位置,然后用 PHP
计算当前位置日出日落时间的办法,但是写起程序来并不容易。
这个问题我曾经在 JS
代码实现白天黑夜引入不同的 CSS - Ben’s Lab 的评论中提到过:
如今,我做出来了,我感觉做这个的过程不是“so difficult”,而是“so so so
so so so so so so so so so so so so difficult”!
首先,看一下粗略的流程图吧:
为什么要用百度地图呢?目前的浏览器都支持定位功能,能够获得准确度比较高的经纬度。但因为已知原因,某些浏览器(如
Chrome)无法使用 HTML 5 内置的定位功能。
百度地图的相关 API 可以到 百度地图 API - 首页
查看,新版的 API 需要获得 AppKey 才能使用。
我偶然发现,百度地图所提供的坐标是经过转换的!
国际经纬度坐标标准为 WGS-84,国内必须至少使用国测局制定的
GCJ-02,对地理位置进行首次加密。百度坐标在此基础上,进行了 BD-09
二次加密措施,更加保护了个人隐私。百度对外接口的坐标系并不是 GPS
采集的真实经纬度,需要通过坐标转换接口进行转换。
我所需要的坐标当然是真实的经纬度坐标了!因此,我就查找将百度坐标转换为原始坐标的方法,却发现百度不提供这种方法。我又到网上找,发现目前没有精确的转换方法,你懂的。同时,我还了解了各种坐标。感兴趣的人可以看一下
关于百度地图坐标转换接口的研究
- Rover.Tang - 博客园 和 [转] 地球坐标
火星坐标 百度坐标 相互转换 。
我找到了一个很不错的 API:http://api.zdoz.net/interfaces.aspx ,转换结果可以精确到小数点后
5
位。但我后来在测试时发现,由于涉及到跨域获取,无法使用。幸好我又找到了一个很好的
JS(原文也提供 PHP 版的)能够解决坐标转换的问题:GPS
坐标互转:WGS-84(GPS)、GCJ-02(Google
地图)、BD-09(百度地图) ,我试了一下,效果很不错,可以精确到小数点后 4
位(PS:据我测试,日出日落时间计算中,经纬度需要精确到小数点后 1
位就行了)。源码并没直接提供百度坐标到 GPS
坐标的转换函数,需要间接弄。
1 2 3 4 5 function bd2GPS (lng,lat ){ var arr2 = GPS .bd_decrypt (lat,lng); var arr3 = GPS .gcj_decrypt (arr2['lat' ], arr2['lon' ]); return {'lng' : arr3['lon' ], 'lat' : arr3['lat' ]}; }
转换为坐标之后,需要将坐标值发送到服务器端进行计算再传回,需要
AJAX。于是我马上在 W3School
补习了 AJAX。我使用的是 GET 方式,这样比较快。
我让 PHP 输出 JSON
语句,然后在客户端上解析并输出。这时我才知道,传回的 JSON 语句需要用
eval() 函数才能解析成功!
PHP 的编写是最难的,倒不是因为代码,而是因为你要考虑很多事情。
**首先,我们要考虑时区问题。**虽然我用的服务器时区为东八区(UTC+8,北京时间所对应的时区),但我想做一个可移植式的
API,这样,无论你的服务器在哪里,你都能在本地收到当前时区对应的时间。
怎么做呢?这时需要客户端发送客户端时区信息。
1 2 var d = new Date ();var localOffset = -d.getTimezoneOffset ()/60 ;
为什么要加负号呢?因为 getTimezoneOffset()
返回的是
UTC - 本地
(我习惯用 UTC 而不是
GMT)。如果不加负号,在北京时间状态下 localOffset
的值是
-8。这个 W3School
并没有说。
然后在 PHP
中获取服务器时区信息,并计算时差。这需要写一个函数,计算服务器时区与 UTC
的时差。这函数是在 php.net 上看到的,链接在代码的第二行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php function get_timezone_offset ($remote_tz , $origin_tz = null ) { if ($origin_tz === null ) { if (!is_string ($origin_tz = date_default_timezone_get ())) { return false ; } } $origin_dtz = new DateTimeZone ($origin_tz ); $remote_dtz = new DateTimeZone ($remote_tz ); $origin_dt = new DateTime ("now" , $origin_dtz ); $remote_dt = new DateTime ("now" , $remote_dtz ); $offset = $origin_dtz ->getOffset ($origin_dt ) - $remote_dtz ->getOffset ($remote_dt ); return $offset ; } ?>
使用时,代码如下:
1 $severOffset = get_timezone_offset ('UTC' )/3600 ;
获取客户端的时间戳($localOffset
是客户端时区):
1 2 $offsetDifference =$severOffset -$localOffset ;$localTimeStamp =time ()-$offsetDifference *3600 ;
然后就可以用到日出日落时间计算了($lat
、$lng
分别为纬度、经度):
1 2 3 4 $sunRiseStamp =date_sunrise ($localTimeStamp ,SUNFUNCS_RET_TIMESTAMP,$lat ,$lng ,90 +50 /60 ,$localOffset );$sunSetStamp =date_sunset ($localTimeStamp ,SUNFUNCS_RET_TIMESTAMP,$lat ,$lng ,90 +50 /60 ,$localOffset );$sunRise =date_sunrise ($localTimeStamp ,SUNFUNCS_RET_STRING,$lat ,$lng ,90 +50 /60 ,$localOffset );$sunSet =date_sunset ($localTimeStamp ,SUNFUNCS_RET_STRING,$lat ,$lng ,90 +50 /60 ,$localOffset );
**其次,我们要考虑日出日落时间次序。**有些地方,在一天之内,日出时间可能会晚于日落时间。当然,你很难找到有这样一个地方,我也不知道有没有,但这很重要,以防万一。代码很简单:
1 2 3 4 5 6 if ($sunSetStamp <$sunRiseStamp ){} else {}
**然后,我们要考虑一天的时间段的划分。**我写的 PHP
在返回日出日落时间同时也会返回当前的时间段。白天和黑夜是很好划分的,但再细分就出问题了:中式的时间段划分方式和西式的不一样(中式:上午,中午,下午,晚上,凌晨;西式:morning,
noon, afternoon, evening, night,其中晚上和凌晨与 evening 和 night
并不一一对应),而且,有些地方的白天或黑夜很短,而如果按小时划分,会出现很多问题。于是,我按照春分日和秋分日时各时间段的位置和比例进行比例划分:
在白天 (day),从日出开始白天的 5/12 ~ 7/12 为中午
(noon),此时间段之前为上午 (morning),之后为下午 (afternoon);
在黑夜 (night),前 1/4 为 evening,后 3/4 为
night;黑夜的前半部分为晚上,后半部分为凌晨。
白天、中午占有两端点值。evening、晚上占有结束端点值。
当时的草稿:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 if ($sunSetStamp <$sunRiseStamp ){ $divideDay =($sunSetStamp +86400 -$sunRiseStamp )/12 ; $divideNight =($sunRiseStamp -$sunSetStamp )/4 ; if (($localTimeStamp ()>=$sunRiseStamp ) || ($localTimeStamp <=$sunSetStamp )) $period ="day" ; else $period ="night" ; if (($localTimeStamp >=$sunRiseStamp && $localTimeStamp <$sunRiseStamp +5 *$divideDay ) || $localTimeStamp <$sunSetStamp -7 *$divideDay ){ $period_exact_chinese ="上午" ; $period_exact_western ="morning" ; } elseif (($localTimeStamp >=$sunRiseStamp +5 *$divideDay && $localTimeStamp <=$sunRiseStamp +7 *$divideDay ) || ($localTimeStamp >=$sunSetStamp -7 *$divideDay && $localTimeStamp <=$sunSetStamp -5 *$divideDay )){ $period_exact_chinese ="中午" ; $period_exact_western ="noon" ; } elseif (($localTimeStamp >$sunSetStamp -5 *$divideDay && $localTimeStamp <=$sunSetStamp ) || $localTimeStamp >$sunRiseStamp +7 *$divideDay ){ $period_exact_chinese ="下午" ; $period_exact_western ="afternoon" ; } elseif ($localTimeStamp >$sunSetStamp && $localTimeStamp <=$sunSetStamp +2 *$divideNight ) $period_exact_chinese ="晚上" ; elseif ($localTimeStamp >$sunSetStamp +2 *$divideNight && $localTimeStamp <$sunRiseStamp ) $period_exact_chinese ="凌晨" ; if ($localTimeStamp >$sunSetStamp && $localTimeStamp <=$sunSetStamp +$divideNight ) $period_exact_western ="evening" ; elseif ($localTimeStamp >$sunSetStamp +$divideNight && $localTimeStamp <$sunRiseStamp ) $period_exact_western ="night" ; } else { $divideDay =($sunSetStamp -$sunRiseStamp )/12 ; $divideNight =($sunRiseStamp +86400 -$sunSetStamp )/4 ; if (($localTimeStamp <$sunRiseStamp ) || ($localTimeStamp >$sunSetStamp )) $period ="night" ; else $period ="day" ; if ($localTimeStamp >=$sunRiseStamp && $localTimeStamp <$sunRiseStamp +5 *$divideDay ){ $period_exact_chinese ="上午" ; $period_exact_western ="morning" ; } elseif ($localTimeStamp >=$sunRiseStamp +5 *$divideDay && $localTimeStamp <=$sunRiseStamp +7 *$divideDay ){ $period_exact_chinese ="中午" ; $period_exact_western ="noon" ; } elseif ($localTimeStamp >$sunRiseStamp +7 *$divideDay && $localTimeStamp <=$sunSetStamp ){ $period_exact_chinese ="下午" ; $period_exact_western ="afternoon" ; } elseif (($localTimeStamp >$sunSetStamp && $localTimeStamp <=$sunSetStamp +2 *$divideNight ) || $localTimeStamp <=$sunRiseStamp -2 *$divideNight ) $period_exact_chinese ="晚上" ; elseif (($localTimeStamp >$sunRiseStamp -2 *$divideNight && $localTimeStamp <$sunRiseStamp ) || $localTimeStamp >$sunSetStamp +2 *$divideNight ) $period_exact_chinese ="凌晨" ; if (($localTimeStamp >$sunSetStamp && $localTimeStamp <=$sunSetStamp +$divideNight ) || $localTimeStamp <=$sunRiseStamp -3 *$divideNight ) $period_exact_western ="evening" ; elseif (($localTimeStamp >$sunRiseStamp -3 *$divideNight && $localTimeStamp <$sunRiseStamp ) || $localTimeStamp >$sunSetStamp +$divideNight ) $period_exact_western ="night" ; }
**然后,我们要考虑是否有极昼极夜。**如果有极昼极夜,date_sunrise
和 date_sunset
返回值为空。所以我们还要验证其返回值是非为空:
1 2 3 4 5 6 if ($sunRise !="" and $sunSet !="" ) { } else { }
那怎么更具体地划分极昼极夜呢?我们可以根据纬度和日期进行划分:春分日(3
月 21 日,以北半球为准)和秋分日(9 月 23
日)无极昼极夜;春分日到秋分日之间,北半球有极昼,南半球有极夜;秋分日到春分日,正好相反。而且极昼极夜时期,时间段只有一个。
我们还要考虑到春分日和秋分日时,南北极点的情况。北极点春分日相当于日出,秋分日相当于日落;南极点正好相反。按上面的规定,均视为白天。
代码如下(放在上面的代码的//此处写极昼极夜代码
处):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 $dateNum =idate ("z" ,$localTimeStamp );if (idate ("L" ,$localTimeStamp )==0 ){ if ($dateNum >=51 && $dateNum <234 ){ if ($lat >0 ){ $period ="day" ; $period_exact_chinese ="白天" ; $period_exact_western ="day" ; } else { $period ="night" ; $period_exact_chinese ="黑夜" ; $period_exact_western ="night" ; } } else { if ($lat >0 ){ $period ="night" ; $period_exact_chinese ="黑夜" ; $period_exact_western ="night" ; } else { $period ="day" ; $period_exact_chinese ="白天" ; $period_exact_western ="day" ; } } } else { if ($dateNum >=52 && $dateNum <235 ){ if ($lat >0 ){ $period ="day" ; $period_exact_chinese ="白天" ; $period_exact_western ="day" ; } else { $period ="night" ; $period_exact_chinese ="黑夜" ; $period_exact_western ="night" ; } } else { if ($lat >0 ){ $period ="night" ; $period_exact_chinese ="黑夜" ; $period_exact_western ="night" ; } else { $period ="day" ; $period_exact_chinese ="白天" ; $period_exact_western ="day" ; } } }
最后,千万别忘了 在代码最前面加上
header('Content-type: application/json; charset=utf-8');
!
输出语句:
1 2 3 4 echo '{"sunrise":"' .$sunRise .'","sunset":"' .$sunSet .'","period":"' .$period .'","period_exact_chinese":"' . $period_exact_chinese .'","period_exact_western":"' .$period_exact_western .'"}' ;echo '{"sunrise":"null","sunset""null","period"' .$period .'","period_exact_chinese":"' . $period_exact_chinese .'","period_exact_western":"' .$period_exact_western .'"}' ;
这个过程是一个极其烧脑的过程:看花括号的时候,总是看错;计算各时间段的范围时,想了半天才想通;构思代码足足花了我三天……不过,总算是大功告成了!
我已把这些东西上传到 GitHub,项目命名为 SunGet,欢迎 Fork 或
Star。地址:https://github.com/DingJunyao/SunGet.git 。其中,master
分支存放的是 sunget.php 和演示文档,GeoSunTime
分支存放的是定位后计算日出日落时间的文档。
我在写自述文档时,还自己翻译成英文版放在中文版下面,好累啊……