BLOG main image
분류 전체보기 (434)
Random (313)
컴퓨터 (35)
Favorites (18)
사진 (9)
아포리즘 (8)
자전거 (30)
여행 (21)

Twitter Updates

    follow me on Twitter
    «   2010/03   »
      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      
    137,160 Visitors up to today!
    Today 6 hit, Yesterday 74 hit
    rss
    'GPS'에 해당되는 글 4건
    2009/12/22 11:55
    아이폰용 어플리케이션 중에는 내부의 GPS 센서를 사용해 이동경로를 기록하는 것들도 있습니다. 이중 제가 사용해 본 BikeMate GPS와 RunKeeper를 비교해 보겠습니다.

    두 프로그램 모두 사용하는 방식은 간단하고 비슷합니다. 출발시 시작(Start)을, 잠시 쉴때는 멈추기(Pause), 마칠때는 저장(Stop)을 누르면 이동거리, 속도, 칼로리 등을 보여주고 웹으로 올릴수도 있도록 되어 있습니다.

    먼저 BikeMate GPS의 화면을 보시겠습니다.
     
    퇴근하면서 측정한 기록입니다. 여러가지 정보를 자세하게 보여주고 있습니다.

    계기판 화면을 터치하면 속도와 고도의 변화를 그래프로 보여줍니다. 또한 처음 계기판의 왼쪽, 오른쪽을 터치하면 칼로리, 거리나 시간을 정해놓고 점점 감소되는 방식으로도 사용할 수 있도록 되어있습니다.

    두번째 경로 화면에서 퇴근길의 경로를 불러온 것입니다. 구글맵에서 경로를 보여주며 구글맵과 마찬가지로 위성사진이나 위성사진과 지도의 혼합화면도 지원해 줍니다만 전체 시간이나 경로의 거리는 보여주지 않습니다. 달리면서 보는 것을 전제하여 만든 모드로 보입니다.

    운동일지의 화면입니다. 달력을 지원하며 해당 날자의 자료를 볼 수 있습니다.
    데이터 탭에서는 이동경로나 사진을 메일로 보내거나 메시업이라는 형태를 통해 웹으로 보낼수 있는데 구글 계정과 Zero One Mashup이라는 자체 계정과 구글 계정의 가입이 필요합니다. 퇴근길의 매시업을 보내면 다음과 같은 구글 메일이 도착합니다.

    링크를 클릭하면 다음과 같이 웹에서도 라이딩 자료를 볼 수 있습니다.


    아래쪽의 슬라이드 바를 이동시키면 특정 지점에서의 속도 뿐 아니라 특정 구간에서의 속도도 볼 수 있습니다.

    다음은 두번째로 구입한 RunKeeper입니다. 처음 실행시키면 다음과 같은 화면이 뜹니다.


    GPS를 통한 입력뿐 아니라 직접 사용자가 자료를 입력할 수도 있고 Activity Type에서는 달리기, 자전거, 수영뿐 아니라 스케이트 보드, 스키, 휠체어 등의 다양한 운동을 종류별로 선택할 수 있습니다. 정품에서는 원하는 곡 목록을 선택해서 음악을 들을 수도 있고 특정한 운동모드를 선택할수도 있습니다.

    특정한 운동모드를 편집하는 화면입니다. 예를 들어 5분간 워밍업후, 1분간 빨리 달리고, 1분 30초간 천천히 달리는 것을 7번 반복하는 것과 같은 운동 방법을 편집할 수 있습니다.

    Activities 탭에서는 저장된 기록들을 볼 수 있고 웹으로 올리거나 지우는 등의 일을 할 수 있습니다.

    실제 운동시와 기록을 선택했을 때 보이는 화면입니다. 이동 거리, 시간, 평속 및 소모한 칼로리를 보여주며 화면을 스왑하면 이동경로를 보여주는 화면이 나타납니다. 실제 운동시에는 단위(Km, mile) 구간별 시간을 보여줍니다.

    지도 화면은 BikeMate와 비슷합니다만, 단위 구간별로 지점을 보여주는 것이 특이합니다.
    RunKeeper는 운동을 마치면 자동으로 운동기록을 서버로 보내도록 되어있습니다. 서버에 접속하면 다음과 같이 달별로 운동기록을 볼 수 있도록 되어있습니다.

    그리고 운동기록을 선택하면 지도와 함께 속도와 고도를 그래프로 보여주고 단위구간별 이동시간도 보여줍니다.

    지도위의 특정 지점을 클릭하면 아래 그래프에서 해당지점을 보여주어 그곳에서의 거리, 고도 등을 알 수 있습니다.

    RunKeeper의 정식 프로그램에는 특정한 시간이나 거리마다 현재의 페이스를 말해주는 기능이 들어가 있습니다. 영어라서 귀에 쏙쏙 들어오지는 않지만 화면을 보지 않고도 현재 내가 어떤 속도로 얼마나 달리고 있는지를 알 수 있는 것은 편한 기능이네요.

    두가지 프로그램을 번갈아 사용해보니 프로그램 마다 하는 일은 비슷하지만 목적이 조금 다르다는 느낌을 받았습니다. 먼저 BikeGPS 프로그램은 자전거의 핸들바에 거치시켜놓고 보면서 달리는 것을 기본으로 만들었고 아이폰의 프로그램으로 대부분의 기능을 사용할 수 있도록 되어 있는 반면 RunKeeper는 음악을 들으면서 기록해 놓고 실제 자료는 웹에서 관리하도록 되어 있습니다.

    BikeMate GPS
    • 장점
      1. 기본 화면에서 좀 더 많은 정보를 보여준다.
        최고속이나 고도등의 값들도 기본 화면에서 보여줍니다.
      2. 웹 화면에서 원하는 구간의 평속과 소모 칼로리를 보여준다.
        예를 들어 특정 오르막에서의 평속을 알아볼수 있겠습니다.
      3. 한국어를 지원한다.
        개발자가 한국분이라는 말이 있는데, 프로그램에도 웹 화면에도 01mia란 이상한 이름의 제작사만 올라와 있고 웹에서는 한국어를 지원하지 않습니다.
    • 단점
      1. 거리가 정확하지 않다.
        가장 큰 단점입니다. 1.2 버전에서는 실제 거리에 비해 과다하게 측정되더니 최근 2.0으로 올라가면서 실제보다 작게 측정됩니다. 따라서 평속, 칼로리 모두 믿을 수 없게 되었습니다. 그리고 화면에서 보여주는 속도와 고도의 그래프도 아주 불규칙합니다.
      2. 웹과의 연동이 불편하다.
        운동 후 일일이 웹에 자료를 보낸 다음 구글 메일에서 만들어진 웹 페이지의 주소를 받아서 보아야 합니다. 1.2 버전에서는 구글 오피스의 스프레드 쉬트에 자료를 올려서 한 단계 더 복잡한 과정이 있었는데 그나마 편해진 셈입니다.
      3. 정식 사용자도 광고를 보아야 한다.
        올려놓은 웹의 결과 화면에는 사이판의 모 리조트 광고가 함께 포함되어 있습니다. 공짜 버전은 이해하더라도 돈을 주고 구입한 정식 사용자가 광고를 보는 것은 불쾌한 경험이네요.
      4. 웹의 지도에서 지명을 볼 수 없다.
        올려놓은 웹의 결과 화면과 같이 형태만 나타나고 지명이나 도로가 보이지 않아서 어느 구간인지 확인하기 어렵게 되어 있습니다. 이건 그리 어렵지 않게 수정할 수 있을것 같습니다만...
    RunKeeper
    • 장점
      1. 웹과의 연동이 편리하다.
        운동을 마치면 자동으로 자료를 전송하기 때문에 일일이 자료를 선택해서 웹으로 보낼 필요가 없습니다. 웹에서는 달별, 주별 총 합과 함께 각각의 기록이 목록에서 보여주므로 편하게 사용할 수 있습니다.
      2. 공짜 버전의 기능도 만족할만 하다.
        공짜 버전에서 빠진 것은 트레이닝 모드, 원하는 곡 목록을 선택해서 듣는 것, 현재의 페이스를 말로 알려주는 것과 사진에 위치정보가 기입되는 기능이라고 합니다. 광고가 화면에  뜨는 것에 크게 개의치 않으면 운동을 기록하는 목적으로는 부족함이 없습니다.
      3. Team 모드를 지원한다.
        아는 분들과 함께 팀을 짜 놓으면 다른 팀원이 얼마나 열심히 운동을 했는지 어디를 다녀왔는지 등을 알 수 있어 친목 도모와 함께 더욱 운동을 열심히 할 수 있는 계기가 되겠습니다.
      4.  여러가지 운동을 지원한다.
        원래 프로그램의 이름이 run keeper 이듯이 달리기를 가장 기본으로 만들어진 프로그램이고 다른 운동들도 선택할 수 있어 여러가지 종목의 운동을 다 하는 분들께 큰 도움이 될것 같습니다.
    • 단점
      • BikeMate GPS의 장점이라고 적어놓은것 이외에 사용하는 데에 크게 불편함은 없습니다.

    총평
    BikeMate GPS의 가장 큰 단점은 기본적인 거리, 속도가 정확하지 않다는 것입니다. 약 9.7Km의 퇴근길을 1.2 버전에서는 10.5Km로, 최근의 2.0 버전에서는 8.5Km로 측정하는 등 기본적인 기능에서 문제가 있습니다. 고도와 속도 그래프도 너무 들쑥날쑥해서 구간에 따른 변화를 알아보기 힘들뿐더러 정확도에도 의심이 갑니다. 이전 버전의 버그 리포트를 홈페이지에 했는데 개발자의 반응도 전혀 없었습니다. 참고로 RunKeeper의 거리는 자전거의 속도계와 거의 동일한 값을 보여주었습니다.

    RunKeeper의 공개 버전을 받아서 써 보시고 Pro 버전의 기능이 필요하시면 업그레이드 하시면 좋을것 같습니다. 저는 앞으로 RunKeeper를 계속 사용할 것 같습니다만 MotionX GPS란 프로그램이 BikeMate와 비슷한 모드에 훌륭한 기능을 보여주는것 같더군요. 여러가지 프로그램에 관심이 있으신 분께서는 한번 살펴보시길...



    2009/12/15 16:39
    주말 직장 단합회가 에덴벨리에서 열렸습니다. 평소 주말 라이딩을 게을리 하고 있던 차 잘 되었다 싶어 대절 버스를 마다하고 자전거를 타고 숙소에 가기로 마음먹었습니다.

    2시 반쯤 광안리 근처의 집에서 출발해서 온천천을 따라 구서동, 범어사를 지나 양산 종합운동장 근처에 도착한게 4시가 좀 넘은 시간. 자전거의 변속 케이블이 끊어져 샾에서 고치고 4시 50분쯤 소위 업힐의 시작점인 어곡 공단 끝쪽의 편의점에서 출발했습니다. 무정차 완주를 목표로 출발했습니다만 한번 10도정도의 경사가 시작되더니 끝까지 완만해지는 일 없이 계속해서 오르막이더군요. 80% 정도 오른 상태에서 결국 퍼져버렸습니다. 잠시 쉬었다 출발해도 한번 퍼진 다리는 돌아올 줄 모르고 날은 점점 어두워져가고 스키장으로 가는 차들은 쉬지 않고 계속 올라오고... 몇번 시도하다 마지막에는 끌바를 해야 했습니다. 소위 짐승급 라이더들은 무정차로 40분대에 올라가고 좀 탄다는 분들은 50분대에 주파하는 것 같습니다만 저는 한시간이 넘어서야 정상까지 올라갔네요.
    대충 업힐을 포기한 지점. 포기할때는 보이지 않는 정상에 좌절했지만 지나고 보니 정상이 그리 멀지 않은 지점이다. 중간 GPS가 제대로 잡히지 않아 36Km가 나왔으나 실제로는 45Km의 거리.



    일요일 단합회를 마치고 돌아오는 내리막 길을 보니 굽이굽이 전날의 고생과 함께 끝까지 올라갈 수 있었을것 같다는 아쉬움이 들기도 했습니다. 좀 더 수련을 해서 언젠가는 꼭 무정차로 가뿐하게 올라가는 날이 오기를...

    PS1. 변속케이블이 끊어져 양산에서 자전거 샾을 찾을 때 아이폰의 지도가 무척 유용했습니다. 지도에서 '자전거'를 검색어로 주었는데 '자전거'가 상호에 들어가지 않은 샵도 검색해 주더군요. 마침 친절하게 수리해주신 샵을 만나서 다행이었습니다.

    PS2. 오늘 우연히 자전거 동호회에서 읽은 내용에 따르면 몸무게*2.2 / (키/2.54)의 값이 2 미만이면 댄싱에 의한 업힐이 아주 우수, 2~2.1이면 우수, 2.2~2.3이면 근력을 키우고, 2.4~2.5이면 다운힐에 우수, 2.5 이상이면 업힐을 피하라고 한답니다. 제 키와 몸무게를 넣고 검사해보면 2.51... 2.2가 되려고만 해도 대학 신입생때 체중이 되어야 하겠네요. 일단 좀 더 열심히 운동을 해야 되겠습니다. -.-
    2009/09/01 17:50
    지난 주말 집사람이 직장일로 경주에서 모임이 있었기에 진작부터 하고 싶었던 한가지를 해 보기로 했습니다. 바로 부산에서 경주까지 자전거를 타고 가는 것이죠. 자출사와 같은 곳에서는 경주까지 왕복하는 사람도 있고 밤새워서 서울까지 가는 사람들도 있습니다만 개인적으로는 그 정도도 충분히 도전이 아닐까 생각했습니다.

    토요일 오후 집사람과 아이는 나중에 자동차로 경주에 가기로 하고 저는 3시쯤 집에서 나섭니다. 보통 동래에서 경주까지 3시간에서 3시간 반 정도를 잡기에 막연히 광안리의 집에서 보문단지까지는 4시간 좀 넘겠구나 라고 생각하고 2시쯤에는 떠나야 되겠구나라고 생각했습니다만 이런 저런 일로 좀 늦게 출발하게 되었습니다. (이 때문에 나중에 고생좀 했습니다)

    도로를 타고 수영로타리를 거쳐 온천천의 시작 지점에서부터는 온천천을 타고 끝나는 구서동 근처에서 다시 도로로 올라와 침례병원과 범어사 앞을 거쳐 양산으로 가는 언덕을 넘어갑니다. 예전 범어사 근처에서 양산으로 넘어가는 길은 구불구불한 왕복 2차선 도로였는데 이제 대부분의 도로가 왕복 6-8차선 정도로 한쪽 차선을 차지하고 달리는데도 큰 무리가 없었습니다. 고가도로를 지나 양산까지는 쭉 내리막길, 제법 속도를 내면서 양산에 도착해서 35번 국도를 달리기 시작합니다. 양산 IC 근처의 편의점에서 초코바 2개와 스포츠 드링크를 사서 먹은 시간이 대략 4시 30분.

    양산에서 언양까지는 많은 분들이 말씀하시듯 맞바람이 세서 고생좀 했습니다. 통도사 근처에서  하도 힘들고 엉덩이가 아파서 잠시 내려보니 싯포스트가 내려와 있습니다. 지난번 높이를 조절하고 싯 클램프를 헐겁게 조였던 모양입니다. 높이를 조절하고 타니 페달링이 훨씬 수월해집니다.  통도사, 삼성 SDI, 메가마트 등을 지나 언양에 도착한 시간이 대략 5시 50분.  이제 슬슬 어두워 지면서 빗방울이 조금씩 떨어지기 시작합니다. 일기예보에는 비올 확률 30%라고 했는데...

    그래도 언양에서부터는 갓길이 넓고 교통량도 많지 않은 편이어서 비교적 수월하게 달릴수 있었습니다. 내렸다 가늘어졌다 하는 빗줄기를 맞으며 얼마나 달렸을까요. 오르막을 하나 오르고 나니 봉계가 나오고 곧 이어 흔히 라이딩하는 사람들이 사진을 많이 찍는 경주시를 알리는 조형물이 나옵니다. 금방 시내가 나올줄 알았지만 달려도 달려도 그대로인 평지를 한참 달려서야 경주 IC와 35번 국도가 만나는 지점에 도착했습니다. 이때가 7시 10분. 깜깜해져서 조심조심 라이트를 보면서 경주 시내를 지나 보문단지에 위치한 숙소에 도착합니다.

    마지막에 긴장이 풀어져 사고가 자주 난다는 말을 들은적이 있어 나름 긴장을 유지하려 했지만 보문단지의 횡단보도 앞에서 멈추면서 왼쪽 클릿을 빼고 서려고 하는데 갑자기 자전거가 오른쪽으로 넘어갑니다. 몸이 피곤해서 제대로 말을 듣지 않은 듯... 완전히 멈춘 다음 천천히 넘어져서 다치지는 않았지만 넘어진쪽 클릿이 빠지지 않아서 아무도 없는 길에 누워 잠시 당황했습니다.



    GPS의 기록입니다. 자전거의 트립 컴퓨터에 기록된 98Km, 평속 22.3Km와 큰 차이는 없습니다. 잠시 물을 마시며 문자를 보낸 시간을 보니 마침 광안리 ~ 양산 IC 인근 ~ 언양 ~ 경주시(경주 IC와 만나는 지점)사이 간격이 대략 1시간 20분씩입니다.

    태어나서 이정도 운동량으로 5시간 정도 계속해서 운동한 것은 처음인것 같습니다. 일요일과 월요일 전반적으로 몸에 기운이 없지만 생각보다 그리 근육통이 심하지도 않아서 다행이네요. 좀 더 밝은 시간에 비나 맞바람이 없다면 언젠가 왕복도 가능할까요?
    2008/04/05 20:49

    지난번에 말씀드린 Holux M-241…

    시간 간격(1, 5, 10, 15, 30, 60, 120초) 혹은 지정 거리(50, 100, 150, 300, 500, 1000m)별로 위치를 저장하고 총 13만개정도의 위치를 저장할 수 있으니 36시간 400초(13만초) – 180일 13시간 20분(15600000초) 정도의 시간, 혹은 6500Km – 130000Km의 거리를 저장할 수 있는 셈입니다.

    BT747 프로그램을 사용하여 맥에서 로그 데이터를 받아 구글 어스에서 받는 것까지는 성공했지만 막상 전체 거리, 속도 등의 자료를 볼 수 없어서 이를 계산하는 프로그램을 짜 보았습니다. 프로그램은 XML의 형식을 가지는 KML 파일을 읽어들여 저장되어 있는 위도, 경도와 시간을 사용하여 이동거리 및 속도를 기록하고 이것을 기초로 간단한 그래프를 보여줍니다.

    프로그램은 GPS 자료의 위도, 경도 차이를 기준으로 거리를 계산합니다. 위도, 경도를 계산하는 방법은 Haversine 방법과 Vincenty 방법 이 알려져 있는데 Vincenty 방법은 지구를 타원으로 측정하여 1m 정도까지 정확하게 계산할 수 있는 모양입니다. 집에서 직장까지의 거리를 Vincenty 방법과 Haversine 방법으로 계산한 결과는 각각 9.212Km, 9.206Km로 대략 60m 정도 차이가 나지만 어차피 GPS가 크게 정확하지 않으리라 생각하고 Haversine 방법으로 거리를 계산했습니다.

    다음은 지난 금요일 자전거를 타고 출퇴근하면서 10초 간격으로 GPS를 기록한 것을 그래프로 나타낸 것입니다. 퇴근때는 GPS를 켜자마자 바로 달려서 초반 부분이 기록되어 있지 않습니다.

    사용자 삽입 이미지
    먼저 출근, 거리는 10.4Km를 달렸고 시간은 31분 정도가 걸렸으면 평속 20Km정도 입니다. 10초 이상 신호등에 걸린 부분에서는 속도가 0이 됩니다. 그리고 후반부에 속도가 상대적으로 감소되는데 이는 산중턱에 있는 직장 근처에서 시작되는 오르막길 때문입니다.

    사용자 삽입 이미지
    다음은 퇴근입니다. 코스가 약간 차이나지만 거리가 9Km로 초반부 GPS 신호를 잡지 못했기 때문으로 생각됩니다. 전박적으로 오전에 비해 속도가 빠른 편이고 경성대학교 부근에서 1번 신호에 잡힌것 말고는 계속 달렸음을 알수 있습니다.

    전체 프로그램의 소스입니다. 그래프는 rubyCocoa를 이용했기 때문에 Leopard이상의 OSX에서 실행해야 합니다. kml파일 목록을 인자로 실행시키면 기본적으로 GPS 기록이 1시간 이상 차이날 때마다 새로운 그래프를 만들어줍니다. KML 파일 이외의 형식도 마지막 부분의 소스를 조금만 수정하면 사용할 수 있도록 만들어져 있습니다.

    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
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    
    require 'rexml/document'
    require 'time'
    require 'osx/cocoa'
    # require 'rubygems'
    # require 'units'
    
    # great resources about distance calculations on web
    # http://williams.best.vwh.net/avform.htm
    # http://www.movable-type.co.uk/scripts/latlong.html
    # http://www.movable-type.co.uk/scripts/latlong-vincenty.html
    # http://ajax.suaccess.org/rubyisms-in-rails/converting-between-degrees-and-radians/
    
    include Math
    include REXML
    
    SUMMARY_KO = { :dist=>"전체 거리 : %.2f Km", :time=>"시간 : %d분 %d초", :max_v=>"최고 속도 : %.2f Km/시", :avg_v=>"평균 속도 : %.2f Km/시" }
    SUMMARY_EN = { :dist=>"Total Distance : %.2f Km", :time=>"Time : %d:%d", :max_v=>"Max Velocity : %.2f Km/hr", :avg_v=>"Avg Velocity : %.2f Km/hr" }
    SUMMARY = SUMMARY_KO
    
    class Numeric
      def to_rad
        self*Math::PI/180
      end
      # add_unit_conversions(:angle => { :radians => 1, :degrees => Math::PI/180 })
      # add_unit_aliases(:angle => { :degrees => [:degree], :radians => [:radian] })
    end
    
    def distVincenty(lat1, lon1, lat2, lon2) 
      a, b = 6378137.0, 6356752.3142
      f = 1/298.257223563;                              # WGS-84 ellipsiod
      l = (lon2-lon1).to_rad
      u1 = atan((1-f) * tan(lat1.to_rad))
      u2 = atan((1-f) * tan(lat2.to_rad))
      sinU1, cosU1 = sin(u1), cos(u1)
      sinU2, cosU2 = sin(u2), cos(u2)
      
      lambda, lambdaP = l, 2*Math::PI
      iterLimit = 19
      while ((lambda-lambdaP).abs > 1e-12 and iterLimit>0) do
        sinLambda, cosLambda = sin(lambda), cos(lambda)
        sinSigma = sqrt((cosU2*sinLambda) ** 2 + (cosU1*sinU2-sinU1*cosU2*cosLambda) ** 2)
        return 0 if (sinSigma==0)                       # co-incident points
        cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda
        sigma = atan2(sinSigma, cosSigma)
        sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
        cosSqAlpha = 1 - sinAlpha ** 2
        cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha
        # if (isNaN(cos2SigmaM)) cos2SigmaM = 0          # equatorial line: cosSqAlpha=0 (§6)
        c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
        lambdaP = lambda
        lambda = l + (1-c) * f * sinAlpha * \
          (sigma + c*sinSigma*(cos2SigmaM+c*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)))
        iterLimit -= 1
      end
    
      return nil if (iterLimit==0)                      # formula failed to converge
    
      uSq = cosSqAlpha * (a*a - b*b) / (b*b)
      biga = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
      bigb = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
      deltaSigma = bigb*sinSigma*(cos2SigmaM+bigb/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM) - \
        bigb/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)))
      b*biga*(sigma-deltaSigma)
    end
    
    def distHaversine(lat1, lon1, lat2, lon2)
      # R = 6371
      dLat = (lat2-lat1).to_rad
      dLon = (lon2-lon1).to_rad
      a = sin(dLat/2) * sin(dLat/2) + cos(lat1.to_rad) * cos(lat2.to_rad) * sin(dLon/2)**2
      c = 2 * atan2(sqrt(a), sqrt(1-a)) 
      6371 * c * 1000;
    end
    
    def velocity(dist, t_int)
      (dist / 1000) / t_int * 3600
    end
    
    def gps_calc(doc, path, loc_f, h_f, t_f, cut_interval = 60 * 5)
      result = []
      rhash = { :data => [] }
      
      pre_lat, pre_lon, pre_h, pre_t = 0, 0, 0, 0
      start_time, total_dist, total_points, time_int, max_v = nil, 0, 0, 0, 0
    
      doc.elements.each(path) do |p|
        lat, lon = loc_f.call(p)
        h = h_f.call(p)
        t = t_f.call(p)
        
        start_time = t if start_time.nil?
        
        if pre_lat != 0 then
          time_int = t - pre_t
          if time_int <= cut_interval then
            dist = distHaversine(lat, lon, pre_lat, pre_lon)
            total_dist += dist
            total_points += 1
            v = velocity(dist, time_int)
            max_v = v if v > max_v
            rhash[:data] << [t - start_time, dist, v]
          end
        end
    
        if time_int > cut_interval then
          avg_v = velocity(total_dist, pre_t - start_time)
          rhash[:start_time] = start_time
          rhash[:total_points]= total_points
          rhash[:total_dist] = total_dist / 1000
          rhash[:time_int] = pre_t - start_time
          rhash[:avg_v] = avg_v
          rhash[:max_v] = max_v 
          result << rhash
          rhash = { :data => [] }      
          total_dist, total_points, max_v, time_int = 0, 0, 0, 0
          pre_lat, pre_lon, start_time = 0, 0, nil
        else
          pre_lat, pre_lon, pre_h, pre_t = lat, lon, h, t
        end    
      end
    
      if pre_lat != 0 then
        avg_v = velocity(total_dist, pre_t - start_time)
        rhash[:start_time] = start_time
        rhash[:total_points]= total_points
        rhash[:total_dist] = total_dist / 1000
        rhash[:time_int] = pre_t - start_time
        rhash[:avg_v] = avg_v
        rhash[:max_v] = max_v 
        result << rhash
      end
      
      result    
    end
    
    def mark_x(gps_data, value, para, emphasis=false)
      position = value * para[:graph_width] / gps_data[:total_dist] + para[:margin]
    
      vstring = value.integer? ? value.to_s : "%.2f" % value
      label = OSX::NSString.alloc.initWithString(vstring)
      font_dict = emphasis ? para[:em_font_dict] : para[:font_dict]
      size = label.sizeWithAttributes(font_dict)
      label.drawAtPoint_withAttributes([position-size.width/2, para[:zero].y-size.height-para[:font_margin]], font_dict)
      OSX::NSRectFill([position, para[:zero].y-para[:font_margin], 1, para[:font_margin]])
    end
    
    def mark_y(gps_data, value, para, emphasis=false)
      position = value * para[:graph_height] / (gps_data[:max_v] * 1) + para[:margin]
      
      vstring = value.integer? ? value.to_s : "%.2f" % value
      label = OSX::NSString.alloc.initWithString(vstring)
      font_dict = emphasis ? para[:em_font_dict] : para[:font_dict]
      size = label.sizeWithAttributes(font_dict)
      label.drawAtPoint_withAttributes([para[:margin]-size.width-para[:font_margin], position-size.height/2], font_dict)
      OSX::NSRectFill([para[:zero].x-para[:font_margin], position, para[:font_margin], 1])
    end
    
    def make_graph(gps_data, para={})
      default = { :width=>700, :height=>500, :mark_int=>60*10, :format=>OSX::NSPNGFileType, :margin=>50, :font_size=>12, :font_margin=>3 }
      dist_scales = [[1, 2, 5, 10, 20, 50, 100], [5, 10, 20, 50, 100, 200, 1000]]
      
      default.update(para)
    
      canvas = OSX::NSBitmapImageRep.alloc.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(nil, default[:width], default[:height], 8, 4, true, false, OSX::NSDeviceRGBColorSpace, 0, 0)
      context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(canvas)
      OSX::NSGraphicsContext.setCurrentContext(context)
    
      # font
      white = OSX::NSColor.whiteColor
      white.set
      yellow = OSX::NSColor.yellowColor
      
      font = OSX::NSFont.fontWithName_size('Helvetica', default[:font_size])
      font_dict = OSX::NSMutableDictionary.alloc.init
      font_dict.setObject_forKey(font, OSX::NSFontAttributeName)
      font_dict.setObject_forKey(white, OSX::NSForegroundColorAttributeName)
      default[:font_dict] = font_dict
    
      em_font = OSX::NSFont.boldSystemFontOfSize(default[:font_size]+2)
      em_font_dict = OSX::NSMutableDictionary.alloc.init
      em_font_dict.setObject_forKey(em_font, OSX::NSFontAttributeName)
      em_font_dict.setObject_forKey(OSX::NSColor.yellowColor, OSX::NSForegroundColorAttributeName)
      default[:em_font_dict] = em_font_dict  
      
      # background gradient
      gradient = OSX::NSGradient.alloc.initWithStartingColor_endingColor(OSX::NSColor.blueColor, OSX::NSColor.blackColor)
      gradient.drawInRect_angle([0, 0, default[:width], default[:height]], 90)
      
      # lines   
      default[:zero] = OSX::NSMakePoint(default[:margin], default[:margin])
      default[:x_end] = OSX::NSMakePoint(default[:width]-default[:margin], default[:margin])
      default[:y_end] = OSX::NSMakePoint(default[:margin], default[:height]-default[:margin])
      path = OSX::NSBezierPath.bezierPath
      path.moveToPoint(default[:x_end])
      path.lineToPoint(default[:zero])
      path.lineToPoint(default[:y_end])
      path.stroke
      
      # labels
      # dist_label = OSX::NSString.alloc.initWithString('Km')
      # dist_size = dist_label.sizeWithAttributes(font_dict)
      # dist_label.drawAtPoint_withAttributes([default[:x_end].x+default[:font_margin], default[:x_end].y-dist_size.height-default[:font_margin]], font_dict)
      # velo_label = OSX::NSString.alloc.initWithString('Km/Hr')
      # velo_size = velo_label.sizeWithAttributes(font_dict)
      # velo_label.drawAtPoint_withAttributes([default[:y_end].x-velo_size.width-default[:font_margin], default[:y_end].y+default[:font_margin]], font_dict)
    
      # 
      default[:graph_width] = default[:width] - default[:margin] * 2
      default[:graph_height] = default[:height] - default[:margin] * 2
      
      # determine x scale units
      scale_index = 0
      (dist_scales[1].size-1).downto(0) { |i| scale_index=i; break if gps_data[:total_dist] > dist_scales[1][i] }
      scale = dist_scales[0][scale_index]
      
      # draw x legends
      # mark_x(gps_data, gps_data[:total_dist], default, true)
      dist = scale
      while (dist < gps_data[:total_dist]) do 
        mark_x(gps_data, dist, default)
        dist += scale
      end
      
      # mark_y(gps_data, gps_data[:max_v], default)
      velo = 10
      while (velo < gps_data[:max_v]) do
        mark_y(gps_data, velo, default)
        velo += 10
      end
      
      # draw graph
      graph = nil
      total = 0
      gps_data[:data].each do |time_int, dist, velo|
        total += dist
        point_x = (total / 1000) * default[:graph_width] / gps_data[:total_dist] + default[:margin]
        point_y = velo * default[:graph_height] / (gps_data[:max_v] * 1) + default[:margin]
        
        if graph.nil? then
          graph = OSX::NSBezierPath.bezierPath
          graph.moveToPoint(OSX::NSMakePoint(point_x, point_y))
        else
          graph.lineToPoint(OSX::NSMakePoint(point_x, point_y))
        end
        
        if (default[:mark_int] != 0) and (time_int % default[:mark_int] == 0) then
          mark_label = OSX::NSString.alloc.initWithString("%d:%02d" % [time_int / 60, time_int % 60])
          mark_size = mark_label.sizeWithAttributes(default[:font_dict])
          mark_label.drawAtPoint_withAttributes([point_x-mark_size.width/2, point_y+default[:font_margin]], default[:font_dict])
          mark = OSX::NSBezierPath.bezierPathWithOvalInRect([point_x-2, point_y-2, 4, 4])
          yellow.set
          mark.stroke
          white.set      
        end
      end
      graph.stroke
      
      # summary
      sum_y = default[:height] - default[:margin] / 2
      dist_label = OSX::NSString.alloc.initWithString(SUMMARY[:dist] % gps_data[:total_dist])
      dist_size = dist_label.sizeWithAttributes(default[:em_font_dict])
      dist_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-dist_size.width, sum_y-dist_size.height], default[:em_font_dict])
      sum_y -= dist_size.height + default[:font_margin]
      time_label = OSX::NSString.alloc.initWithString(SUMMARY[:time] % [gps_data[:time_int]/60, gps_data[:time_int]%60])
      time_size = time_label.sizeWithAttributes(default[:em_font_dict])
      time_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-time_size.width, sum_y-time_size.height], default[:em_font_dict])
      sum_y -= time_size.height + default[:font_margin]
      maxv_label = OSX::NSString.alloc.initWithString(SUMMARY[:max_v] % gps_data[:max_v])
      maxv_size = maxv_label.sizeWithAttributes(default[:em_font_dict])
      maxv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-maxv_size.width, sum_y-maxv_size.height], default[:em_font_dict])
      sum_y -= maxv_size.height + default[:font_margin]
      avgv_label = OSX::NSString.alloc.initWithString(SUMMARY[:avg_v] % gps_data[:avg_v])
      avgv_size = avgv_label.sizeWithAttributes(default[:em_font_dict])
      avgv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-avgv_size.width, sum_y-avgv_size.height], default[:em_font_dict])
    
      
      canvas.representationUsingType_properties(default[:format], nil).rubyString  
    end
    
    if ARGV.size > 0 then
      ARGV.each do |a|
        doc = Document.new(open(a))
        
        loc_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[0..2] }
        h_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[2] }
        t_f = proc { |e| Time.parse(e.elements['TimeStamp'].elements['when'].text).localtime }
        
        r = gps_calc(doc, 'kml/Document/Folder/Folder/Placemark', loc_f, h_f, t_f, 60*60)
        r.each do |result|
          open("graph_#{result[:start_time].strftime('%Y%m%d%H%M')}.png", 'wb') { |f| f.write(make_graph(result))} 
        end
      end
    end
    
    prev"" #1 next