本文寫于2015-08-06 11:05。由于技術進步,其中的描述不一定適用于現在,請自行定奪。
PHP從5起,新增了關于日出和日落的函數:
date_sunrise
、date_sunset
(PHP5.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)無法使用HTML5內置的定位功能。
百度地圖的相關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坐標的轉換函數,需要間接弄。
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,這樣,無論你的服務器在哪里,你都能在本地收到當前時區對應的時間。
怎么做呢?這時需要客戶端發送客戶端時區信息。
var d = new Date();
var localOffset = -d.getTimezoneOffset()/60;
為什么要加負號呢?因為getTimezoneOffset()
返回的是UTC-本地(我習慣用UTC而不是GMT)。如果不加負號,在北京時間狀態下localOffset
的值是-8。這個 W3School 并沒有說。
然后在PHP中獲取服務器時區信息,并計算時差。這需要寫一個函數,計算服務器時區與UTC的時差。這函數是在php.net上看到的,鏈接在代碼的第二行:
<?php
/** http://php.net/manual/zh/function.timezone-offset-get.php
* Returns the offset from the origin timezone to the remote timezone, in seconds.
* @param $remote_tz;
* @param $origin_tz; If null the servers current timezone is used as the origin.
* @return int;
*/
function get_timezone_offset($remote_tz, $origin_tz = null) {
if($origin_tz === null) {
if(!is_string($origin_tz = date_default_timezone_get())) {
return false; // A UTC timestamp was returned -- bail out!
}
}
$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;
}
//Examples:
// This will return 10800 (3 hours) ...
//$offset = get_timezone_offset('America/Los_Angeles','America/New_York');
// or, if your server time is already set to 'America/New_York'...
//$offset = get_timezone_offset('America/Los_Angeles');
// You can then take $offset and adjust your timestamp.
//$offset_time = time() + $offset;
?>
使用時,代碼如下:
$severOffset = get_timezone_offset('UTC')/3600;
獲取客戶端的時間戳($localOffset
是客戶端時區):
$offsetDifference=$severOffset-$localOffset;
$localTimeStamp=time()-$offsetDifference*3600; //這是客戶端的時間戳
然后就可以用到日出日落時間計算了($lat
、$lng
分別為緯度、經度):
$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);
其次,我們要考慮日出日落時間次序。有些地方,在一天之內,日出時間可能會晚于日落時間。當然,你很難找到有這樣一個地方,我也不知道有沒有,但這很重要,以防萬一。代碼很簡單:
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、晚上占有結束端點值。
當時的草稿:
代碼:
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
返回值為空。所以我們還要驗證其返回值是非為空:
if($sunRise!="" and $sunSet!="") {
//此處寫非極晝極夜代碼
}
else {
//此處寫極晝極夜代碼
}
那怎么更具體地劃分極晝極夜呢?我們可以根據緯度和日期進行劃分:春分日(3月21日,以北半球為準)和秋分日(9月23日)無極晝極夜;春分日到秋分日之間,北半球有極晝,南半球有極夜;秋分日到春分日,正好相反。而且極晝極夜時期,時間段只有一個。
我們還要考慮到春分日和秋分日時,南北極點的情況。北極點春分日相當于日出,秋分日相當于日落;南極點正好相反。按上面的規定,均視為白天。
代碼如下(放在上面的代碼的//此處寫極晝極夜代碼
處):
$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');
!
輸出語句:
//非極晝極夜
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分支存放的是定位后計算日出日落時間的文檔。
我在寫自述文檔時,還自己翻譯成英文版放在中文版下面,好累啊……