获取当前所在位置并计算当前位置日出日落时间(JS+PHP)

本文写于2015-08-06 11:05。由于技术进步,其中的描述不一定适用于现在,请自行定夺。

PHP从5起,新增了关于日出和日落的函数:date_sunrisedate_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、晚上占有结束端点值。

当时的草稿:

草稿1
草稿2

代码:

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_sunrisedate_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分支存放的是定位后计算日出日落时间的文档。

我在写自述文档时,还自己翻译成英文版放在中文版下面,好累啊……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 1.经纬度计算: 经度差与地方时差算经度——地方时每相差1小时,经度相差1°;纬差法与正午太阳高度算纬度——正午太...
    大王姐姐阅读 902评论 0 0
  • 1 序: 很多新接触GIS的人员对地图投影以及坐标系统很难理解,甚至做GIS开发做了好几年的人也有这方面的疑惑,地...
    三维GIS那点事_王跃军阅读 17,294评论 3 43
  • 1.真正不羁的灵魂不会真的去计较什么,因为他们的内心深处有国王般的骄傲。——杰克•凯鲁亚克《在路上》
    名说阅读 194评论 0 0
  • 自从入了农药的坑,大部分的比赛都在用王昭君。 首先,选择王昭君的原因: 颜值高啊!漂亮啊!冻一下可以慢慢打!操作性...
    新校路上的红绿灯阅读 132评论 0 0
  • 多情东风入春来 春廷满林胭脂萼 无香何慊醉溪影 粉黛最宜锦绣堆 2017、04、03 陶韵
    陶韵阅读 149评论 0 0