本文寫于2015-08-06 11:05。由于技術(shù)進(jìn)步,其中的描述不一定適用于現(xiàn)在,請(qǐng)自行定奪。
PHP從5起,新增了關(guān)于日出和日落的函數(shù):
date_sunrise
、date_sunset
(PHP5.1.2起還有date_sun_info
函數(shù),有興趣的可以看看),這對(duì)于要根據(jù)日出日落時(shí)間改變網(wǎng)頁內(nèi)容的人來說是一個(gè)福音。我由此想到了可以先用JS獲取當(dāng)前所在位置,然后用PHP計(jì)算當(dāng)前位置日出日落時(shí)間的辦法,但是寫起程序來并不容易。
這個(gè)問題我曾經(jīng)在 JS代碼實(shí)現(xiàn)白天黑夜引入不同的CSS - Ben's Lab 的評(píng)論中提到過:
如今,我做出來了,我感覺做這個(gè)的過程不是“so difficult”,而是“so so so so so so so so so so so so so so so difficult”!
首先,看一下粗略的流程圖吧:
為什么要用百度地圖呢?目前的瀏覽器都支持定位功能,能夠獲得準(zhǔn)確度比較高的經(jīng)緯度。但因?yàn)橐阎?,某些瀏覽器(如Chrome)無法使用HTML5內(nèi)置的定位功能。
百度地圖的相關(guān)API可以到 百度地圖API - 首頁 查看,新版的API需要獲得AppKey才能使用。
我偶然發(fā)現(xiàn),百度地圖所提供的坐標(biāo)是經(jīng)過轉(zhuǎn)換的!
國際經(jīng)緯度坐標(biāo)標(biāo)準(zhǔn)為WGS-84,國內(nèi)必須至少使用國測局制定的GCJ-02,對(duì)地理位置進(jìn)行首次加密。百度坐標(biāo)在此基礎(chǔ)上,進(jìn)行了BD-09二次加密措施,更加保護(hù)了個(gè)人隱私。百度對(duì)外接口的坐標(biāo)系并不是GPS采集的真實(shí)經(jīng)緯度,需要通過坐標(biāo)轉(zhuǎn)換接口進(jìn)行轉(zhuǎn)換。
我所需要的坐標(biāo)當(dāng)然是真實(shí)的經(jīng)緯度坐標(biāo)了!因此,我就查找將百度坐標(biāo)轉(zhuǎn)換為原始坐標(biāo)的方法,卻發(fā)現(xiàn)百度不提供這種方法。我又到網(wǎng)上找,發(fā)現(xiàn)目前沒有精確的轉(zhuǎn)換方法,你懂的。同時(shí),我還了解了各種坐標(biāo)。感興趣的人可以看一下 關(guān)于百度地圖坐標(biāo)轉(zhuǎn)換接口的研究 - Rover.Tang - 博客園 和 [轉(zhuǎn)]地球坐標(biāo) 火星坐標(biāo) 百度坐標(biāo) 相互轉(zhuǎn)換 。
我找到了一個(gè)很不錯(cuò)的API: http://api.zdoz.net/interfaces.aspx,轉(zhuǎn)換結(jié)果可以精確到小數(shù)點(diǎn)后5位。但我后來在測試時(shí)發(fā)現(xiàn),由于涉及到跨域獲取,無法使用。幸好我又找到了一個(gè)很好的JS(原文也提供PHP版的)能夠解決坐標(biāo)轉(zhuǎn)換的問題:GPS坐標(biāo)互轉(zhuǎn):WGS-84(GPS)、GCJ-02(Google地圖)、BD-09(百度地圖),我試了一下,效果很不錯(cuò),可以精確到小數(shù)點(diǎn)后4位(PS:據(jù)我測試,日出日落時(shí)間計(jì)算中,經(jīng)緯度需要精確到小數(shù)點(diǎn)后1位就行了)。源碼并沒直接提供百度坐標(biāo)到GPS坐標(biāo)的轉(zhuǎn)換函數(shù),需要間接弄。
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']};
}
轉(zhuǎn)換為坐標(biāo)之后,需要將坐標(biāo)值發(fā)送到服務(wù)器端進(jìn)行計(jì)算再傳回,需要AJAX。于是我馬上在 W3School 補(bǔ)習(xí)了AJAX。我使用的是GET方式,這樣比較快。
我讓PHP輸出JSON語句,然后在客戶端上解析并輸出。這時(shí)我才知道,傳回的JSON語句需要用eval()函數(shù)才能解析成功!
PHP的編寫是最難的,倒不是因?yàn)榇a,而是因?yàn)槟阋紤]很多事情。
首先,我們要考慮時(shí)區(qū)問題。雖然我用的服務(wù)器時(shí)區(qū)為東八區(qū)(UTC+8,北京時(shí)間所對(duì)應(yīng)的時(shí)區(qū)),但我想做一個(gè)可移植式的API,這樣,無論你的服務(wù)器在哪里,你都能在本地收到當(dāng)前時(shí)區(qū)對(duì)應(yīng)的時(shí)間。
怎么做呢?這時(shí)需要客戶端發(fā)送客戶端時(shí)區(qū)信息。
var d = new Date();
var localOffset = -d.getTimezoneOffset()/60;
為什么要加負(fù)號(hào)呢?因?yàn)?code>getTimezoneOffset()返回的是UTC-本地(我習(xí)慣用UTC而不是GMT)。如果不加負(fù)號(hào),在北京時(shí)間狀態(tài)下localOffset
的值是-8。這個(gè) W3School 并沒有說。
然后在PHP中獲取服務(wù)器時(shí)區(qū)信息,并計(jì)算時(shí)差。這需要寫一個(gè)函數(shù),計(jì)算服務(wù)器時(shí)區(qū)與UTC的時(shí)差。這函數(shù)是在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;
?>
使用時(shí),代碼如下:
$severOffset = get_timezone_offset('UTC')/3600;
獲取客戶端的時(shí)間戳($localOffset
是客戶端時(shí)區(qū)):
$offsetDifference=$severOffset-$localOffset;
$localTimeStamp=time()-$offsetDifference*3600; //這是客戶端的時(shí)間戳
然后就可以用到日出日落時(shí)間計(jì)算了($lat
、$lng
分別為緯度、經(jīng)度):
$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);
其次,我們要考慮日出日落時(shí)間次序。有些地方,在一天之內(nèi),日出時(shí)間可能會(huì)晚于日落時(shí)間。當(dāng)然,你很難找到有這樣一個(gè)地方,我也不知道有沒有,但這很重要,以防萬一。代碼很簡單:
if($sunSetStamp<$sunRiseStamp){
//此處寫黑夜在一整天之內(nèi)的代碼
}
else{
//此處寫白天在一整天之內(nèi)的代碼
}
然后,我們要考慮一天的時(shí)間段的劃分。我寫的PHP在返回日出日落時(shí)間同時(shí)也會(huì)返回當(dāng)前的時(shí)間段。白天和黑夜是很好劃分的,但再細(xì)分就出問題了:中式的時(shí)間段劃分方式和西式的不一樣(中式:上午,中午,下午,晚上,凌晨;西式:morning,noon,afternoon,evening,night,其中晚上和凌晨與evening和night并不一一對(duì)應(yīng)),而且,有些地方的白天或黑夜很短,而如果按小時(shí)劃分,會(huì)出現(xiàn)很多問題。于是,我按照春分日和秋分日時(shí)各時(shí)間段的位置和比例進(jìn)行比例劃分:
- 在白天(day),從日出開始白天的5/12~7/12為中午(noon),此時(shí)間段之前為上午(morning),之后為下午(afternoon);
- 在黑夜(night),前1/4為evening,后3/4為night;黑夜的前半部分為晚上,后半部分為凌晨。
- 白天、中午占有兩端點(diǎn)值。evening、晚上占有結(jié)束端點(diǎn)值。
當(dāng)時(shí)的草稿:
代碼:
if($sunSetStamp<$sunRiseStamp){ //黑夜在一整天之內(nèi)
$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{ //白天在一整天之內(nèi)
$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
返回值為空。所以我們還要驗(yàn)證其返回值是非為空:
if($sunRise!="" and $sunSet!="") {
//此處寫非極晝極夜代碼
}
else {
//此處寫極晝極夜代碼
}
那怎么更具體地劃分極晝極夜呢?我們可以根據(jù)緯度和日期進(jìn)行劃分:春分日(3月21日,以北半球?yàn)闇?zhǔn))和秋分日(9月23日)無極晝極夜;春分日到秋分日之間,北半球有極晝,南半球有極夜;秋分日到春分日,正好相反。而且極晝極夜時(shí)期,時(shí)間段只有一個(gè)。
我們還要考慮到春分日和秋分日時(shí),南北極點(diǎn)的情況。北極點(diǎn)春分日相當(dāng)于日出,秋分日相當(dāng)于日落;南極點(diǎn)正好相反。按上面的規(guī)定,均視為白天。
代碼如下(放在上面的代碼的//此處寫極晝極夜代碼
處):
$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.'"}';
這個(gè)過程是一個(gè)極其燒腦的過程:看花括號(hào)的時(shí)候,總是看錯(cuò);計(jì)算各時(shí)間段的范圍時(shí),想了半天才想通;構(gòu)思代碼足足花了我三天……不過,總算是大功告成了!
我已把這些東西上傳到GitHub,項(xiàng)目命名為SunGet,歡迎Fork或Star。地址:https://github.com/DingJunyao/SunGet.git。其中,master分支存放的是sunget.php和演示文檔,GeoSunTime分支存放的是定位后計(jì)算日出日落時(shí)間的文檔。
我在寫自述文檔時(shí),還自己翻譯成英文版放在中文版下面,好累啊……