HOME システム関連アウトソーシング 開発実績 出張サポート アプリケーション お問合せ 雑記帳  

Game City 看板

Game City 看板


 Game City ・・・何かゲームセンターの名称のようですが、ハボロネの南端に位置するボツワナ最大級のショッピングモールのことです。

 出店店舗は南アフリカ資本のチェーン店ばかり・・・国内のショッピングモールは何処行っても規模の違いこそあれ入っている店舗は同じです。
 「ボツワナ資本はないの?頑張ってよ!」という気分になるのですが、人口が少ないのでチェーン店を展開するような大規模経営はボツワナでは難しいのかもしれません。
 

・CHOPPIES
 ボツワナで最も店舗数の多いスーパー。どこの街に行っても大概あります。数少ないボツワナ生まれのお店と聞いて最初応援してたのですが、経営はインド人だということを後で知って少しがっかり。
 いや、インド人でも全然OKなんですけど、ボツワナ人も頑張ってるな!って思ってたもので・・・。

CHOPPIES


 

・SHOPRITE
 南アフリカ最大のスーパーチェーン店。個人的に一番良く利用するスーパーです。他チェーンのスーパーと比較して野菜の鮮度がまともな気がします。

SHOPRITE


 

・CAPE UNION MART
 アウトドアグッズショップ。まともなメーカーの登山用品を扱っているのは嬉しいのですが値段が高い。
 きちんと調べたわけではないですが、日本の3割~5割増しといった印象。

CAPE UNION MART


 

・WOOLWORTH
 衣料の他、一部食料品も売ってます。オーストラリア資本の高級志向店。私は紅茶を此処で買っていますが・・・トワイニングのイングリッシュブレックファースト、それもティーバッグ。
 多分ボツワナでは美味い紅茶は手に入りません。

CAPE UNION MART


 

・Mr. Price
 衣料品店ですが、台所用品や風呂用品などの日用品を専門的に扱う店舗もあります。 価格を売りにしているような店名ですが、さほど安くはありません。

PEP STORE


 

・PEP STORE
 衣料品を中心に家庭用雑貨も扱っています。低価格。CHOPPIESの次くらいに全国各所で見かけます。

PEP STORE


 

・CLICKS
 ドラッグストア。重宝してます。

CLICKS


 

・GAME STORES
 ホームセンター。電化製品から食料品まで、この店だけで大概の生活用品は揃います。
 Game CityにあるからGAME STORESという名称なのかと思っていましたが、ここも南アフリカのチェーン店のようなので、たまたま「Game」が共通しただけのようです。

GAME STORES


 

・番外編
 通路の真ん中にあるサングラスショップ。大きなショッピングモールには、このような半露店的なショップがいくつかあります。
 上記Game Storesの写真を撮ってたら「私も撮ってよ!」と頼まれたので、おまけで掲載。

Street Stall

 前回までで、円の大きさが地球を1周するような大きなものにならない限りは等距離の円を同心円状に描くことが出来るようになりました。
 今回は、
 ・円の半径を地球を1周するような規模に設定した場合、本来とは逆の領域が選択されてしまう場合がある
 この不具合に対応したいと思います。

 ポリゴンを描く際に選択する領域をA、選択していない方の領域をBとした時、Google Maps APIではAとBの面積の差があまりないような場合には、A・Bのうち北側に位置する方の領域を選択する傾向にあるようです。そのため、南半球にある地点を円の中心点にする場合はAの方が南側になりますからBが選択されてしまいます。また、例えば東京から15000kmの等距離円などBの面積が明らかにAよりも小さくなる場合にはBが選択されます。

 google.maps.Polygonメソッドに「指定した緯度・経度がポリゴン内部に含まれるように領域選択する」オプションがあればいいのになあ・・・と思うのですが、残念ながら備わっていません。
 でも、そのオプションの有無を探している際に、google.maps.geometryライブラリのポリゴン関数にcontainsLocationというものがあるのを見つけました。

 ・Maps Javescript API ジオメトリ ライブラリより抜粋
 containsLocation(point:LatLng, polygon:Polygon) 
 この関数は、指定の地点がポリゴンの内部または境界に存在すれば、true を返します。
 
 これを利用すれば、次のようなロジックでポリゴン描画すれば、上記ケースにも対応出来るのではないかと考えました。

 1. 半径の大きい円のポリゴンをダミーで描く(visible=falseとして表示させない)
 2. 円の中心点がダミーポリゴンの内部に含まれているかどうかをcontainsLocation関数で調べる
 3. 内部に含まれている場合はそのままでOKなのでダミーと同じものをもう一度描き表示させる。
 4. 内部に含まれていない場合は逆の領域Bがポリゴンとして選択されてしまったことになる。
  領域Aを選択させるには、一旦地球(地図)の全領域を覆うポリゴンを描いた後、領域Bを中抜け
  領域としてくり抜いてしまえば良い。
 
 コードにすると次のようになります

   //地図全体を覆うポリゴン用座標データ(反時計回り)
   var polyWholeWorld = [
      new google.maps.LatLng(0, 180),
      new google.maps.LatLng(-90, 180),
      new google.maps.LatLng(-90, 90),
      new google.maps.LatLng(-90, 0),
      new google.maps.LatLng(-90, -90),
      new google.maps.LatLng(-90, -180),
      new google.maps.LatLng(0, -180),
      new google.maps.LatLng(90, -180),
      new google.maps.LatLng(90, -90),
      new google.maps.LatLng(90, 0),
      new google.maps.LatLng(90, 90),
      new google.maps.LatLng(90, 180),
      new google.maps.LatLng(0, 180)
   ];
   //ダミーのポリゴンを作成   
   var dummyCircle = new google.maps.Polygon({
      paths: polyCoords,  //ポリゴン領域の位置情報(緯度・経度)の配列(時計回り)
      visible: false
   });
   //中心点(myPoint)がダミーポリゴン内部に含まれているかどうかをチェック
   var innerFlag = google.maps.geometry.poly.containsLocation(myPoint, dummyCircle);

   if (innerFlag) {
      //内部にある場合は、そのままのデータでOK
      polyPath = [polyCoords];  
   } else {
      //内部にない場合は、地球全領域からダミーポリゴンの領域をくり抜く
      polyPath = [polyWholeWorld, polyCoords];  //polyWholeWorldとpolyCoordsは回りが逆
   }
   //等距離円の描画
   myCircle = new google.maps.Polygon({
      paths: polyPath,
      strokeWeight: 0,
      fillColor: red,
      fillOpacity: 0.4
   });
   myCircle.setMap(map);

 後は前回同様に内側の円領域を切り抜く処理を加える必要があります。通常のケースでは単に1つ内側の円領域データ(配列)を逆順にすれば良いのですが、「内側の円の段階で本来とは逆の領域が選択されてしまっている場合にはどうすればいいのだろうか?」という疑問が浮かびましたが、このケースでも正しい領域を描くために使用したデータ(地球全体を選択して領域Bをくり抜くパターン)をそのまま全て逆順にする次のような手法で上手くいきました。

 地球全体領域-外円の逆領域(領域B1)-{地球全体領域-内円の逆領域(領域B2)}

 この式を展開すれば 

 内円の逆領域(領域B1)-外円の逆領域(領域B1)

 これで良かったのか・・・でも考え方としては上式の方が分かりやすい気がするので、結果は同じですが今回は上式を採用しました。

計算・描画結果

計算・描画結果 ボツワナの首都(ハボロネ)から3000km,5000km,8000km,10000kmの同心円

 上手く出来たようです。しかし、ボツワナから10,000kmでは日本に全然届かない・・・やっぱり遠いなあ。

 前回、一応それっぽい同心円を描いてみたものの、等距離円の距離が世界地図規模(千km単位)となると計算誤差が大きくなるため、今回はもう少し正確性の高い計算方法を探索します。

 以前に位置情報(緯度・経度)の分かっている2点間の距離を知りたいと思った時には、その求め方が記載されたサイトはWeb上に多数存在し、結構簡単に見つけられたのですが、今回必要としている「ある1点からの距離がrである点の緯度・経度を求める方法」はなかなか見つけられず、一番頼りになる国土地理院のサイトにも記載なし。「もうここはおとなしくgoogle.maps.Circle利用で妥協するか(同心円の色が重なってしまいますが)」と諦めかけたところで漸く見つけました・・・Vincenty法(順解法)。一旦見つけてしまえば「何で今まで見つけられなかったんだろう?」と思うのですが、探し物にはよくあることですね。
 始点の緯度・経度、始点からの距離・方位角を、このVincenty法(順解法)の式に代入することで到達点の緯度・経度が求まります。
 同心円を描くには始点の周り360度の点を全て求める必要があるので、以下のように関数化しました。

  //Vincenty法(順解法)
  function VincentyDirect(p1,deg1,s) {
/*
     	引数
        p1    : 中心となる点(点1)の緯度・経度(google.maps.LatLngクラス)
        deg1  : 求める点の方角(真北からの時計回りの角度。北:0、東:90、南:180、西:270)
        s     : 点1からの距離(m)
*/
     var p2                         //求める点(点2)の緯度・経度(google.maps.LatLngクラス)
     var a = 6378137.0;             //長半径(赤道半径)
     var f = 1.0 / 298.257223563;   //扁平率(WGS84)
     var b = a * (1 - f);           //短半径(極半径)
     var phi1,phi2;                 //点1、点2の緯度
     var L1,L2;                     //点1、点2の経度
     var L;                         //点1、点2の経度差
     var alpha1,alpha2;
     var alpha;
     var U1,U2;
     var lambda
     var sigma;
     var sigma1,formersigma;
     var sigmam,deltasigma;
     var epsilon = 1.0e-12;         //計算精度(sigmaの許容誤差)
     var sinalpha,cosalpha2;
     var u2,A,B,C;
     var cnt=0;

     phi1 = calRadian(p1.lat());
     L1 = calRadian(p1.lng());
     alpha1 = calRadian(deg1);

     U1 = Math.atan((1 - f)*Math.tan(phi1));
     sigma1 = Math.atan2(Math.tan(U1),Math.cos(alpha1));
     sinalpha = Math.cos(U1)*Math.sin(alpha1);
     cosalpha2 = 1-Math.pow(sinalpha,2);
     u2 = cosalpha2*((Math.pow(a,2)-Math.pow(b,2))/Math.pow(b,2));
     A = 1+u2/16384*(4096+u2*(-768+u2*(320-175*u2)));
     B = u2/1024*(256+u2*(-128+u2*(74-47*u2)));

     sigma = s/(b*A);
     do {
        sigmam = (2*sigma1+sigma)/2;
        deltasigma = B*Math.sin(sigma)
                       *(Math.cos(2*sigmam)
                         +1/4*B*(Math.cos(sigma)*(-1+2*Math.pow(Math.cos(2*sigmam),2))
                                 -1/6*B*Math.cos(2*sigmam)*(-3+4*Math.pow(Math.sin(sigma),2))*(-3+4*Math.pow(Math.cos(2*sigmam),2))));
        formersigma = sigma;
        sigma = s/(b*A)+deltasigma;
        cnt++;
        if (cnt>100) {
           alert("error");
           return null;
        }
     } while (Math.abs(sigma-formersigma)>epsilon);

     phi2 = Math.atan2((Math.sin(U1)*Math.cos(sigma)+Math.cos(U1)*Math.sin(sigma)*Math.cos(alpha1))
                     ,((1-f)*Math.sqrt(Math.pow(sinalpha,2)+Math.pow((Math.sin(U1)*Math.sin(sigma)-Math.cos(U1)*Math.cos(sigma)*Math.cos(alpha1)),2))));
     lambda = Math.atan2(Math.sin(sigma)*Math.sin(alpha1),(Math.cos(U1)*Math.cos(sigma)-Math.sin(U1)*Math.sin(sigma)*Math.cos(alpha1)));
     C = f/16*cosalpha2*(4+f*(4-3*cosalpha2));
     L = lambda-(1-C)*f*sinalpha*(sigma+C*Math.sin(sigma)*(Math.cos(2*sigmam)+C*Math.cos(sigma)*(-1+2*Math.pow(Math.cos(2*sigmam),2))));
     L2 = L + L1;
     alpha2 = Math.atan(sinalpha/(-1*Math.sin(U1)*Math.sin(sigma)+Math.cos(U1)*Math.cos(sigma)*Math.cos(alpha1)));
     p2 = new google.maps.LatLng(calDegree(phi2),calDegree(L2));
     return p2;
  }

 アークタンジェントの関数はMath.atan()とMath.atan2()の2種類あり、これらを使い分ける必要がありますが、Wikipediaではその辺も親切に解説されています。
 Vincenty法での緯度・経度を計算するサイト(数箇所)での計算結果と比較することで検証しましたが、誤差はほとんどなかったので上記ソースで多分大丈夫かと思います。なお、扁平率はWGS84のものを採用しましたが、GRS80の扁平率を採用しても計算結果はほとんど変わりません。

 と、ここまで作成した段階で残念な発見・・・同様の計算をする関数がgoogle.maps.geometryライブラリに用意されていました。

 ・Maps Javescript API ジオメトリ ライブラリより抜粋
 computeOffset() を使用すると、特定の角度、出発地点、移動距離(メートル単位)から、目的地の座標を計算できます。

 Google Maps APIを読み込む際に、

<script type="text/javascript" src="http://maps.google.com/maps/api/js?language=jp&key=取得したAPIキー&libraries=geometry"></script>

 といった具合にlibraries=geometryのパラメータを追記するだけで利用可能です。google.maps.Circleも当関数を利用しているようです。
 computeOffset()と自作の上記関数とで誤差があるか検証したところ、低緯度の鉛直方向(北半球なら南側)の誤差が大きくなるようで、ハボロネ(ボツワナの首都)から5000km地点計算時には約31kmのズレが出ました。5000kmで31kmならば誤差は1%未満、地図上ではほとんど分からないレベルですが、原因が気になるのでいろいろ調べてみるとgoogle.maps.geometryライブラリでは地球を完全な球体とみなして計算していることが分かりました(自作関数の扁平率をゼロにして再度検証した結果は両者一致しました)。
 大差ないとはいえ、自作関数の方が扁平率を考慮している分だけより正確ということになるのか・・・わざわざ作った甲斐がありました。computeOffset()ではなく、こちらを採用します。
 
 ここまで出来てしまえば、地図に落とす処理は簡単です。
 今回のテスト用html、Javascriptのソースを以下に記載します。 

・concentriccircles_test2.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>concentric circles on the map</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="http://maps.google.com/maps/api/js?language=jp&libraries=geometry"></script>
<style type="text/css">
<!--
.circletitle {
   font-size:12px;
   color:#FFFF00;
   padding-left:20px;
   white-space: nowrap; 
}
.circletitle input {
   width:40px;
}
.circletitle input[type="button"] {
   width:60px;
   font-size:14px;
}

-->
</style>
<script type="text/javascript" src="js/concentriccircles_test2.js"></script>
</head>

<body style="margin:0px;" onload="initialize()">
  <div id="top_bar" style="background-color:#333399;padding:5px;">
  <table>
  <tr>
    <th style="color:#FFFFFF;text-align:left;padding-left:20px;" colspan="7" style="font-size: 16px;">中心となる地点から一定距離にある点を求めて地図上に同心円を描く(テスト2)~Vincentyの順解法利用~</th>
  </tr>
  <tr>
    <td class="circletitle">circle1</td>
    <td class="circletitle">circle2</td>
    <td class="circletitle">circle3</td>
    <td class="circletitle">circle4</td>
    <td class="circletitle">circle5</td>
    <td colspan="2">&nbsp;</td>
  </tr>
  <tr>
    <td class="circletitle">
        <input type='text' id="f_distance0">km
        <input type='text' id="f_color0">
    </td>
    <td class="circletitle">
        <input type='text' id="f_distance1">km
        <input type='text' id="f_color1">
    </td>
    <td class="circletitle">
        <input type='text' id="f_distance2">km
        <input type='text' id="f_color2">
    </td>
    <td class="circletitle">
        <input type='text' id="f_distance3">km
        <input type='text' id="f_color3">
    </td>
    <td class="circletitle">
        <input type='text' id="f_distance4">km
        <input type='text' id="f_color4">
    </td>
    <td class="circletitle">
       <input type="button" id="btnDrawCircle" name="btnDrawCircle" onclick="drawCircle()" value="draw">
    </td>
    <td class="circletitle">
       <input type="button" id="btnResetCircle" name="btnResetCircle" style="color:#ff0000;" onclick="resetCircle()" value="reset">
    </td>
  </tr>
  </table>
  </div>
  <div id="map_canvas" style="margin:5px;height:600px"></div>
</body>
</html>

・concentriccircles_test2.js

  var map;
  var svg;
  var Gaborone = new google.maps.LatLng(-24.6091799,25.7900739);
  var iniZoom = 4;
  var pointMarker = new google.maps.Marker();
  var circleInfo = new Array();
  var myCircle = new Array();
  var polygons = new Array();
  var drawFlag;

  function initialize() {

     drawFlag = false;
     var dColor = ["blue","green","red","yellow","magenta"];
     var dDistance = [500,1000,1500];

     for (var i = 0; i <= 4; i++) {
        if (dDistance[i]) {
           document.getElementById("f_distance"+i).value = dDistance[i];
        }
        if (dColor[i]) {
           document.getElementById("f_color"+i).value = dColor[i];
        }
     }

     var myOptions = {
        zoom: iniZoom,
        center: Gaborone,
        mapTypeControl: false,
        mapTypeId: google.maps.MapTypeId.ROADMAP
     };
     map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

     google.maps.event.addListener(map, "click", function(clickevent) {
        var myPoint = new google.maps.LatLng(clickevent.latLng.lat(), clickevent.latLng.lng());
        setPointMarker(myPoint);
     });

  }

  function setPointMarker(location, pile) {

    if(!pointMarker.getMap()){
       pointMarker = new google.maps.Marker({
          position: location,
          map: map,
          draggable: true,
       });
    }else{
       pointMarker.setPosition(location);
    }

  }

  function setCircleInfo() {
     
     var curDistance,curColor,curPplText;

     circleInfo = new Array();

     for (var i = 0; i <= 4; i++) {
        curDistance = parseFloat(document.getElementById("f_distance"+i).value)*1000;
        curColor = document.getElementById("f_color"+i).value;
        if (!curColor) {
           curColor = "#ffffff";
        }
        if (isFinite(curDistance)) {
           circleInfo.push({"distance":curDistance,"color":curColor});
        }
     }

     circleInfo.sort(function(a,b){
        if(a.distance<b.distance) return -1;
        if(a.distance > b.distance) return 1;
        return 0;
     });

  }

  function resetCircle() {

     var i;

     for (var i = 0; i < myCircle.length; i++) {
        if (myCircle[i]) {
           myCircle[i].setMap(null);
        }
     }
     myCircle.length = 0;
     
     if (pointMarker.getMap()) {
        pointMarker.setMap(null);
     }
     drawFlag = false;
  }

  function drawCircle() {

     var polyCoords = new Array();
     var polyPath = new Array();
     var degpitch = 0.5;//0.5度ピッチで計算

     if (!pointMarker.getMap()) {
        alert("中心となる点にマーカーを設置して下さい。");
        return 0;
     }

     if (drawFlag) {
        return 0;
     }

     var myPoint = pointMarker.getPosition();

     setCircleInfo();

     for (var i = 0; i < circleInfo.length; i++) {
        var polyCoord = new Array();
        //中心点から指定の距離だけ離れた地点の座標を計算し、配列に収納(ポリゴン用)
        for (var theta = 0; theta < 360; theta += degpitch) {  //thetaは真北からの角度(時計回り)
           polyCoord.push(VincentyDirect(myPoint,theta,circleInfo[i].distance));
        }
        polyCoord.push(polyCoord[0]);
        polyCoords[i] = polyCoord;

        polyPath = [polyCoords[i]];
        //最内以外の円描画時は、1つ内側の円の座標を反対周りにした配列を付加することでドーナツ型ポリゴンにする
        if (i > 0) {
           polyPath.push(polyCoords[i-1].slice().reverse());
        }

        myCircle[i] = new google.maps.Polygon({
           paths: polyPath,
           strokeWeight: 0,
           fillColor: circleInfo[i].color,
           fillOpacity: 0.4
        });
        myCircle[i].setMap(map);

     }

     drawFlag = true;

  }

  function calRadian(deg){
     return deg * Math.PI / 180.0;
  }

  function calDegree(rad){
     return rad * 180 / Math.PI;
  }

  //Vincenty法(順解法)
  function VincentyDirect(p1,deg1,s) {
/*
     	引数
        p1    : 中心となる点(点1)の緯度・経度(google.maps.LatLngクラス)
        deg1  : 求める点の方角(真北からの時計回りの角度。北:0、東:90、南:180、西:270)
        s     : 点1からの距離(m)
*/
     var p2                         //求める点(点2)の緯度・経度(google.maps.LatLngクラス)
     var a = 6378137.0;             //長半径(赤道半径)
     var f = 1.0 / 298.257223563;   //扁平率(WGS84)
     var b = a * (1 - f);           //短半径(極半径)
     var phi1,phi2;                 //点1、点2の緯度
     var L1,L2;                     //点1、点2の経度
     var L;                         //点1、点2の経度差
     var alpha1,alpha2;
     var alpha;
     var U1,U2;
     var lambda
     var sigma;
     var sigma1,formersigma;
     var sigmam,deltasigma;
     var epsilon = 1.0e-12;         //計算精度(sigmaの許容誤差)
     var sinalpha,cosalpha2;
     var u2,A,B,C;
     var cnt=0;

     phi1 = calRadian(p1.lat());
     L1 = calRadian(p1.lng());
     alpha1 = calRadian(deg1);

     U1 = Math.atan((1 - f)*Math.tan(phi1));
     sigma1 = Math.atan2(Math.tan(U1),Math.cos(alpha1));
     sinalpha = Math.cos(U1)*Math.sin(alpha1);
     cosalpha2 = 1-Math.pow(sinalpha,2);
     u2 = cosalpha2*((Math.pow(a,2)-Math.pow(b,2))/Math.pow(b,2));
     A = 1+u2/16384*(4096+u2*(-768+u2*(320-175*u2)));
     B = u2/1024*(256+u2*(-128+u2*(74-47*u2)));

     sigma = s/(b*A);
     do {
        sigmam = (2*sigma1+sigma)/2;
        deltasigma = B*Math.sin(sigma)
                       *(Math.cos(2*sigmam)
                         +1/4*B*(Math.cos(sigma)*(-1+2*Math.pow(Math.cos(2*sigmam),2))
                                 -1/6*B*Math.cos(2*sigmam)*(-3+4*Math.pow(Math.sin(sigma),2))*(-3+4*Math.pow(Math.cos(2*sigmam),2))));
        formersigma = sigma;
        sigma = s/(b*A)+deltasigma;
        cnt++;
        if (cnt>100) {
           alert("error");
           return null;
        }
     } while (Math.abs(sigma-formersigma)>epsilon);

     phi2 = Math.atan2((Math.sin(U1)*Math.cos(sigma)+Math.cos(U1)*Math.sin(sigma)*Math.cos(alpha1))
                     ,((1-f)*Math.sqrt(Math.pow(sinalpha,2)+Math.pow((Math.sin(U1)*Math.sin(sigma)-Math.cos(U1)*Math.cos(sigma)*Math.cos(alpha1)),2))));
     lambda = Math.atan2(Math.sin(sigma)*Math.sin(alpha1),(Math.cos(U1)*Math.cos(sigma)-Math.sin(U1)*Math.sin(sigma)*Math.cos(alpha1)));
     C = f/16*cosalpha2*(4+f*(4-3*cosalpha2));
     L = lambda-(1-C)*f*sinalpha*(sigma+C*Math.sin(sigma)*(Math.cos(2*sigmam)+C*Math.cos(sigma)*(-1+2*Math.pow(Math.cos(2*sigmam),2))));
     L2 = L + L1;
     alpha2 = Math.atan(sinalpha/(-1*Math.sin(U1)*Math.sin(sigma)+Math.cos(U1)*Math.cos(sigma)*Math.cos(alpha1)));
     p2 = new google.maps.LatLng(calDegree(phi2),calDegree(L2));
     return p2;
  }

 各距離の円の色が重ならないよう、外側の円のポリゴンはドーナツ状にしています。Google Maps APIのポリゴンでは、内側領域のデータを外側とは逆回りの配列で追記することで中抜けの形を描画することが出来るので、外側の円の描画時にはその1つ内側のポリゴン配列を逆回りにしてPolygonクラスにデータを渡しています(上記ソース125~129行目)。

 そして、上のHTMLを開いて描画させた結果が以下の図です。

計算・描画結果

計算・描画結果 ボツワナの首都(ハボロネ)から1000km,2000km,3000kmの同心円


 
 同心円の色も重ならず、また半透明なので下の地図が見えなくなってしまうこともなくバッチリです。
 「これで完成!」と思ったのですが・・・・・・。

 距離を10000kmなど大きくして試してみると・・・

計算・描画結果(長距離)

計算・描画結果 ボツワナの首都(ハボロネ)から3000km,5000km,10000kmの同心円

 このように一番外側の距離円(もはや円ではありませんが)のポリゴンが本来とは逆の領域を選択してしまっています。その関係で内側の円には外側の円の色(今回の場合は赤)が重なってしまいました。
 中心点や距離をいろいろ変えて描画させてみたところ、どうやらGoogle Maps APIのポリゴンは地球を1周して南北に2分割するような大きな領域を描く際には北半球側を選択するようです。

 ところが悔しいことに、google.maps.Circleで同条件の同心円を描いた場合は以下の図のように南半球でもきちんと選択されています

google.maps.Circle

同条件でにてgoogle.maps.Circleを利用した結果


 「色が重なる程度の小さなことは気にせず、おとなしくgoogle.maps.Circleを使いなさい!」ということなんでしょうか?・・・いえいえ、まだ諦めません。次回へつづく

 久々に本職のシステム関連の投稿を・・・。

 ある日、配属先の同僚から次のような相談を受けました。
「今、ここに首都(ハボロネ)からの距離が500km、1000km、1500kmの同心円になっている地図があるんだけど、この中心をハボロネに限らず任意の点から作図出来るようなアプリ作れない?」
 地図上の距離に関しては以前に「緯度・経度情報のある2点間の距離」を計算する関数を作成しているので「ああ、出来るよ!」と気軽に答えたものの・・・細部にこだわったこともあり(相手はそこまで求めていないのですが)、結構苦労しました。

 まず、地図に関しては中心とする地点の選びやすさを考慮してGoogle Maps APIを利用することにしました。Google Mapにはgoogle.maps.Circleという便利なクラスがあり、これを使えば簡単に同心円が作れます。なので、円を描くだけならそれでいいのです。
 しかし、見本には各円に色が塗ってある。もちろんCircleクラスでも円を塗りつぶすことが出来ますが、複数の円を描く同心円では色が重なってしまいます。透過度をゼロにして内側から描けば色は重なりませんが、今度は地図が見えなくなってしまい場所が何処だか分からなくなってしまうので・・・。結局自分で計算することにしました。

 「点Aからの距離がrである点の座標(緯度・経度)」を求める計算(点Bは無数に存在)。最初考えたのはAから真東にだけ離れた点(同緯度)と真北にrだけ離れた点(同経度)を最初に求めて、その2点を通る楕円上の点の座標を求める方法です(下のイメージ図)。

点Aからの距離がrである点の座標(緯度・経度)を求める計算手法(1)~イメージ~

点Aからの距離がrである点の座標(緯度・経度)を求める計算手法(1)~イメージ~

 以前作成した2点間の距離を計算する関数を使えば、点Aから緯度1度分離れた点及び経度1度分離れた点の距離が計算出来るので、それを基準に上図の点B・点Cの位置情報を求め、後は三角関数を使用して楕円上の点を計算していくというものです。実際、ネットで調べてみると同じような理論で計算されている方もちらほらといらっしゃるので、これでOKかな?と目論んでいたものの・・・。
 以下のコードが当手法で計算しポリゴンとして地図上に落としたものです。

  var map;
  var mapCenter = new google.maps.LatLng(-23, 23);
  var pointMarker = new google.maps.Marker();
  var polygons = new Array();

  function initialize() {

     var myOptions = {
        zoom: 3,
        center: mapCenter,
        mapTypeControl: true,
        mapTypeId: google.maps.MapTypeId.ROADMAP
     };
     map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

     google.maps.event.addListener(map, "click", function(clickevent) {
        var myPoint = new google.maps.LatLng(clickevent.latLng.lat(), clickevent.latLng.lng());
        setPointMarker(myPoint);
     });


  }

  function setPointMarker(location, pile) {

    if(!pointMarker.getMap()){
       pointMarker = new google.maps.Marker({
          position: location,
          map: map,
          draggable: true,
       });
    }else{
       pointMarker.setPosition(location);
    }
  }

  function drawCircle() {

     if (!pointMarker.getMap()) {
        alert("中心となる点にマーカーを設置して下さい。");
        return 0;
     }

     var myPoint = pointMarker.getPosition();
     var pN = new google.maps.LatLng(myPoint.lat()-0.5,myPoint.lng()); //中心点から北へ0.5度
     var pS = new google.maps.LatLng(myPoint.lat()+0.5,myPoint.lng()); //中心点から南へ0.5度
     var pW = new google.maps.LatLng(myPoint.lat(),myPoint.lng()-0.5); //中心点から西へ0.5度
     var pE = new google.maps.LatLng(myPoint.lat(),myPoint.lng()+0.5); //中心点から東へ0.5度
     var dLat = getDistance(pN,pS); //緯度1度分の距離
     var dLng = getDistance(pW,pE); //距離1度分の距離
     var curLat,curLng;
     var cLat,cLng;
     var polyCircle = new Array;
     var r = 3000000;//今回は3000kmで計算
     var degpitch = 0.5; //今回は同心円上の点は0.5度ピッチで計算

     for (var theta = 0; theta < 360; theta += degpitch) {
        curLng = myPoint.lng() + (r / dLng) *  Math.cos(calRadian(theta));  //経度(X座標)
        curLat = myPoint.lat() + (r / dLat) *  Math.sin(calRadian(theta));  //緯度(Y座標)
        polyCircle.push(new google.maps.LatLng(curLat,curLng));
     }
     polyCircle.push(polyCircle[0]);

     var myCircle = new google.maps.Polygon({
        paths: polyCircle,
        fillColor: '#00FFFF',
        fillOpacity: 0.5,
        strokeColor: '#00FFFF',
        strokeOpacity: 1.0,
        strokeWeight: 1
     });     
     myCircle.setMap(map);

     //比較用 Google Maps API Circleクラスでの描画
     var myCircleB = new google.maps.Circle({
        map: map,
        center: myPoint,
        radius: r,
        fillOpacity: 0,
        strokeColor: "#FF0000",
        strokeOpacity: 1,
        strokeWeight: 2
     });

  }

  //度数→ラジアン変換
  function calRadian(deg){
     return deg * Math.PI / 180.0;
  }

  //2点間の距離を計算(GRS80)
  function getDistance(p1, p2) {
     var a = 6378137.0;             //長半径
     var f = 1.0 / 298.257222101;   //扁平率
     var b = a * (1 - f);           //短半径
     var e2 = (Math.pow(a, 2) - Math.pow(b, 2)) / Math.pow(a, 2);
     var dy = calRadian(p1.lat() - p2.lat());
     var dx = calRadian(p1.lng() - p2.lng());
     var my = calRadian((p1.lat() + p2.lat()) / 2.0);
     var w = Math.sqrt(1.0 - e2 * Math.pow(Math.sin(my), 2));
     var m = (a * (1 - e2)) / Math.pow(w, 3);
     var n = a / w;

     return Math.sqrt(Math.pow(dy * m, 2) + Math.pow(dx * n * Math.cos(my), 2));
  }

 一見良さげなのですが、Circleクラスで描いた円(下図の赤い線)と比較すると結構ズレがあります。
 真東・真西の位置はぴったり合っているようなのですが、特に南東・南西方向のズレは大きく100km以上です(下の拡大図参照)。これだと都市地図等の近距離なら気にならない程度の誤差で収まるかもしれませんが、世界地図レベルとなると実用には耐えられそうもありません。

計算・描画結果

計算・描画結果 ボツワナの首都(ハボロネ)から3000kmの円
Google Maps APIのCircleクラス利用で描いたもの(赤い線)とは結構ズレがあります。
この地図を別窓で表示


誤差の大きい部分を拡大(中心から南東方向)

ズレの大きい部分を拡大(中心から南東方向)。100km以上の誤差があります。これではちょっと・・・


 考えてみたら、経度1度当たりの距離は高緯度になるほど短くなるのに、それを考慮せずに1度当たりの距離を過大評価してしまっている状態ですから、等距離の円も高緯度では実際よりも萎んだ形となってしまうのも当然です。
 また、地球は真球ではなく楕円体なので同じ「緯度1度分」であっても北へ1度と南へ1度とでは距離が違うのですね。つまり、緯度差と距離は比例しないので比率換算では緯度は求められない(上記の方法はそこが間違い)。
 参考までに、上記の2点間の距離を計算する関数を利用して、東京から北へ10度離れた点と南へ10度離れた点までの距離を調べてみたところ、

 ・東京(35.69,139.69)から北へ1度離れた点までの距離:1110479.2m
 ・東京(35.69,139.69)から南へ1度離れた点までの距離:1108641.5m

 2km近い距離差となりました。意外だったのは北側の方が長かったことです。地球は赤道半径の方が極半径より長いので、「低緯度側(北半球の場合は南側)の方が当然長くなるだろうと計算前には思っていたのですが結果は逆でした。プログラムが間違っているのかとも思いましたが、国土地理院のサイトでの計算でもほぼ同じ結果を得られたので間違いないようです。

 緯度は地球の中心からの角度で決めたもの(地心緯度)ではなく、楕円体面の法線と赤道の角度で決めたもの(地理緯度)だそうで、そのため高緯度ほど緯度1度当たりの距離が長くなるようで・・・、私にはなんだか難しいですが、いずれにしても上記の方法では駄目なので、考え直しが必要です。次回へつづく

 プラ(Pula)とはボツワナの現地語(ツワナ語)で雨のことです。
 ケッペンの気候区分によればボツワナの国土の大半はステップ気候に属しており(一部は砂漠気候)、雨が少なく作物が育ちにくい環境にあります。

ケッペンの気候区分(ボツワナ)

ケッペンの気候区分(ボツワナ)


 それだけに、たまに降る雨(プラ)はとても貴重で天からの贈り物。通貨がプラであることや、国旗のデザインに水色が採用されていることからも、ボツワナの人々にとって雨がいかに大切であるかが分ります。
100プラ紙幣

通貨もプラ。上写真は100プラ紙幣(約1,000円)


ボツワナ国旗

ボツワナ国旗。水色は雨(水)を表します。

 10月から4月くらいまでが雨季に当たりますが、首都ハボロネの年間降水量は400㎜程度と東京の4分の1ほどで、雨季といってもそんなに降るわけではありません(だからステップ気候なわけですが)。
 しかし、今シーズンは雨が多いです。ラニーニャの影響だそうですが、特にこの2ヶ月くらいは雨が降らない日の方が多いです。昨年は干ばつで農業が大打撃を受けたようなので、こちらの人々は「今年は雨が多くていいね!」と言っていますが、もともと雨が少ない国なので排水の設備が整っておらず、各所で水害が発生しています。
 私は自転車通勤なのでレインコート必携ですが、雨のおかげでさほど暑くならないのは助かります。昨年の夏は記録的な暑さで気温46度の日も何日かあったそうなので・・・。

ここ最近は雨ばかり

ここ最近は雨ばかり

 ボツワナは「黄熱に感染する危険のある国」には含まれていないので、日本-ボツワナ間の往復にはイエローカード(黄熱予防接種証明書)は不要です。ただ、アフリカには予防接種推奨国がいくつもあるので、今後それらの国へ渡航することを考えると、イエローカードを取得しておいた方が無難です。ボツワナでも黄熱の予防接種を受けられるとの話を聞いて、休暇期間中に受けてきました。

 黄熱予防接種を実施している病院は多くないので(日本も同様ですが)、教えてもらった情報を頼りにまずはハボロネ中心部にあるPrincess Marina Hospitalへ。でも、大きな病院でどこへ行ったらいいのか良く分かりません・・・いくつか尋ね回った末に案内されたのは何故か会計窓口。ここでは黄熱の予防接種は受けられないようで、料金(28.2プラ=約300円)だけを支払い。この病院はワクチンの管理をしている受付センターのような所なのかシステムが良く分かりません。どこなら受けられるのかを聞いたら「Village Clinicが近いよ」とのことなので、そのVillage Clinicへ。
 コンビ(ミニバス)を利用すれば早いのですが、路線が良く分からないのでPrincess Marina Hospitalから歩きました(徒歩30分程)。Village Clinicには10時くらいに到着しましたが、黄熱予防接種は14時からとのこと。その間、他の場所へ行ったりして時間を潰しました。
 そして14時にVillage Clinicを再訪すると、既に結構な人数の来訪者。この時間帯は「黄熱予防接種アワー」のようで全員が私と同じ目的での来院でした。1時間くらい待ちましたが外国人の私でも問題なくワクチン接種・イエローカード取得出来ました。

 受付1箇所で受けられないので時間がかかって面倒ですが、日本なら1万円以上する黄熱の予防接種を300円程度で受けられるというのは破格の安さです。

Princess Marina Hospital

Princess Marina Hospital
病院が混むのは日本だけではないようで、患者さんで溢れかえるような様子でした。

黄熱予防接種の領収書

料金は28.2プラ(約300円)。破格の安さです

Village Clinic

Village Clinic
病院というより保健所といった雰囲気

Village Clinic

取得したイエローカード。昨年から1回接種すれば生涯有効となりましたが、何故か有効期限10年(変更前の期間)になっています。多分、変更されたことを担当者が知らないのだと思います。のんびりした国ですから・・・

 ボツワナを含む南部アフリカ地域の伝統的食材の1つとして「モパネワーム(Mopane Worm)」があります。モパネという木に多くいることから名づけられたそうで、蛾の幼虫です。通称はパニ(Pani)、多分モパネの「パネ」が訛ったものではないかと思われます。

 採ってきた幼虫の内臓をとって乾燥させると数ヶ月は保存が効くので、古くから貴重なタンパク源として、ごく当たり前のフードとして(決してゲテモノではなく)食されています。とはいっても、現代では普通に肉類が流通していますので、一部地域を除けば日常的な食材ではなく、「季節もの」の位置づけです。
 今(1月)がちょうどその季節で、スーパーには売っていませんが(今の所、まだ見かけていません)、露店等で手に入るようです。

 日本でも長野県の一部地域ではザザムシ(オケラの幼虫)を食べる習慣が残っているようですが、イモムシを食べるというのは少なからず抵抗があります。さらに私の場合は、イモムシの見た目が苦手でニョッキですらアウトなくらいなので、かなりハードルの高い食べ物です。でも、「ボツワナに赴任した以上、一度は試さなければ!」と半ば義務のように感じていました。

 そして先日、同僚が入手してきてくれたのでチャレンジ! 目を瞑って食べました・・・味は煮干しに似ています。ただ、妙な後味が残るのが気になって仕方がありません。煮干しだって後味が残りますが気にならないのですから、これは気持ちの問題なんだろうと思います。

 こちらの人は、これをそのままスナック的感覚で食べる他、水に戻してトマトソースで煮るなどの料理をして食することもあるそうなのですが、そちらの方は遠慮しておきます。私には無理です。一応、ミッションクリアということで・・・。

パニ

パニ

 今回滞在したサニパス(Sani Pass)はドラケンスバーグ山脈にある峠の1つです。ドラケンスバーグとはアフリカーンス語で「竜の峰々」という意味だそうですが(英語のドラゴンに通じるのでイメージしやすい)、その名の通り荒々しい山容で、この山脈の尾根が南アフリカとレソトの国境となっています。また、当山脈周辺のマロティ・ドラケンスバーグ公園は世界遺産に登録されています。

 滞在初日に登ったタバナ・ヌトレニャナ山もこの山脈中にありますが、主稜からはやや外れているためドラケンスバーグ特有の険しい山々や絶壁の景色はあまり見られません。そこで2日目、3日目は、サニパス周辺でそんな景色が見られる所を散策しました。絶壁に沿って歩いてみたり、尾根を登って眺めを楽しんだり。タバナ・ヌトレニャナ登山の疲れが残っていたので午前中のみの散策で午後は宿でのんびりしていました。この時期は雨季に当たるようで、両日とも午前は晴れていても午後からは雨になったので、遠出をしなくて正解・・・というよりも初日にタバナ・ヌトレニャナ山に登っておいて本当に良かったです。

 4泊したサニパスでの滞在を終え、相乗りタクシーで麓のモコトロンへ(100マロチ=約850円)。モコトロン着は11時過ぎ。もうマセル行きのバスは出てしまっているので、ここに宿泊しようかと思ったのですが、天気が良くなかったので「それなら行けるところまでいってしまおう」とButha Buthe(マセル―モコトロンの中間に位置するレソト第2の都市。)行きのミニバスに搭乗。行きに乗った大型バスと違い、ミニバスは飛ばしまくりで怖い!しかも、スピードの出し過ぎが原因かは分かりませんが、途中で「何だか煙臭いなあ」と思ったらどうやら故障したようで・・・後輪の車軸から煙が出てストップしてしまいました。運転手は修理を試みていましたが、結局直せず1時間以上待った後に代替の車が到着して乗り換え。安全運転で走っていた方が到着早かったんじゃないの? まあ、この手のトラブルはアフリカでは日常茶飯事、事故にならなくて良かったです。Butha Butheからは2台のミニバスを乗り継いで、夜8時過ぎにマセルに到着。

 サニパスは本当に景色が良い所でしたが、移動に時間がかかるのが難点。ドラケンスバーグの見どころは南アフリカ側に多いみたいなので、本当はダーバン側からアクセスしたかったです。JICA隊員の南アフリカ渡航禁止規定さえなければ・・・。

レソトの伝統的住居

レソトの伝統的住居。農村部ではたくさん見かけました。


ドラケンスバーグ山脈の絶壁

ドラケンスバーグ山脈の絶壁


谷を挟んだこちら側もやはり絶壁です

谷を挟んだこちら側もやはり絶壁です


なぜか絶壁に佇むヤギたち

なぜか絶壁に佇むヤギたち


草を食む牛たち

草を食む牛たち


シュバシコウ

シュバシコウ。コウノトリの一種のようです


Hodgeson's Peak (North)

Hodgeson's Peak (North)に登ってみました。3,251m


Hodgeson's Peak North山頂からの眺望(1)

Hodgeson's Peak (North) 山頂からの眺望(1)


Hodgeson's Peak North山頂からの眺望(2)

Hodgeson's Peak (North) 山頂からの眺望(2)


Hodgeson's Peak North山頂からの眺望(3)

Hodgeson's Peak (North) 山頂からの眺望(3) 本当は写真左のHodgeson's Peak (South)にも登りたかったのですが(標高3,256mなのでNorthよりも5mだけ高い)、向かう途中で馬に乗った少し怖めの羊飼い2名に追われた関係で断念。逃げ場の無さそうな形状なので・・・。写真右側の山へ登り何とか逃げ切りました


帰りのミニバスの車窓から

帰りのミニバスの車窓から。レソトにはテーブルマウンテンのような丘がそこら中にあります


故障したミニバス

乗っていたミニバス。途中後輪車軸から煙が出てストップ。「アフリカあるある」でしょうか・・・


レソトのナンバープレート

レソトのナンバープレート。国旗にも採用されている伝統的な帽子「バソトハット」がデザインされています

 サニパスには4泊することにしたので、終日行動出来る日は3日間。そのいずれかにタバナ・ヌトレニャナ山に登りたい。タバナ・ヌトレニャナ山は直接距離でもサニパスから片道13km近くあるので結構遠いです。しかも決められた道や標識があるわけではないのでガイドを雇うのが無難ですが(500マロチ=約4,300円)、前日に申し込まなければならないので当日の天気を見て判断出来ないのが難点。ドミトリーで部屋が一緒になったフランス人は「ガイドを雇わずGPSを頼りに登ったよ」と言っていたので、それも不可能ではなさそう・・・。

 そんな状況で迎えた初日。この数日胃腸の調子が悪く体調は今一歩でしたが、天気は快晴。「とりあえず途中まで様子見で行ってみよう」と、雨具などの装備を整えて出発しました。フランス人ハイカーが持っていたGPSには登山地図が入っているのか山頂までのコースが点線で書かれていましたが、私のGarminにはOSMデータを入れているだけなので、情報は目的地(タバナ・ヌトレニャナ山)の位置情報のみ。コースも途中の山容も分からないので「まずは目の前の山に登って見よう。そうすればタバナ・ヌトレニャナ山が見えるだろう」と直線的なルートを選択。

タバナ・ヌトレニャナ山までのルート

タバナ・ヌトレニャナ山までのルートは、このGPSの位置情報のみ

 「登った先は尾根づたいになっていてそのまま山頂まで行けるかな?」なんて淡い期待を抱いていたのですが、そんなに甘くはありませんでした。登った先の向こう側は谷になっていて同じくらいの高さを下る羽目に・・・。幸いなことに歩いているうちに体調は上向いてきたので、もう少し奥まで行ってみようと奥の山に登りますが、これが結構きつい! 

kniphofia

この山域各所で見かけた花。kniphofiaという南アフリカ原産の植物のようです。

 コースが分からないと高低差のロスが大きいルート取りとなってしまうのでハードです。また、想定外だったのが結構高い所でもそこら中に羊・ヤギがいること。一応、私も元羊飼いですから羊がいても全然問題ないのですが、羊がいるということは牧羊犬もいるわけで見知らぬ者(=私)が近づくと吠えまくって追ってきて無茶苦茶怖いんです! 犬を避けた迂回ルートを何度もとらざるを得ませんでした。羊飼いに会うのもまた厄介で・・・基本的には友好的で、犬が吠えるのも抑えてくれるのですが例外なく金品を求めてくる。まあ、要求に応える必要はないのですが、とにかく面倒なので接触を避けるため、遠くに見えるとこれまた迂回・・・かなりのロスになったと思います。

羊がいるのどかな風景です。牧羊犬さえいなければ

羊がいるのどかな風景です。牧羊犬さえいなければ・・・


標高3000ⅿ超でも草さえ生えていればそこに羊あり

標高3000ⅿ超でも、草が生えていればそこに羊あり

 出発から3時間半後、ようやくタバナ・ヌトレニャナ山が見えました。でも、まだはるか先・・・。もう引き返そうかなとも考えましたが、せっかくここまで来て戻るのも勿体ないし、天候も崩れる気配はないので続行することにしました。

タバナ・ヌトレニャナ山

ようやくタバナ・ヌトレニャナ山が見えました(写真中央)


タバナ・ヌトレニャナ山頂

タバナ・ヌトレニャナ山頂。「ここが山頂!」って感じがあまりしない山です。

 結局、休憩なしの歩き詰めで5時間超、往路にして既にクタクタでしたが何とかタバナ・ヌトレニャナ山頂に到着しました。最後の1kmくらい、頼みもしないのに羊飼いの少年2人がついてきましたが、「山頂で写真撮影頼めるからいいかな」と拒絶しませんでした。予想通りお金を要求してきましたが、20マロチ(約170円)で満足してくれたので撮影代と考えればまあOKでしょう。

タバナ・ヌトレニャナ山頂にて新旧羊飼いのツーショット

タバナ・ヌトレニャナ山頂にて新旧羊飼いのツーショット


山頂からの眺望

山頂からの眺望


毛布を着た羊飼い

この辺りの羊飼いは、ほぼ100%毛布を着用しています。夏でも朝晩は冷え込みとはいえ、さすがに昼は暑いと思うのですが・・・もはや着ているというより肌と一体化している感じ?

 復路は、往路の教訓を生かして高低差が少なくなるようなルートを心掛けたのですが、それでもなおハードで、牧羊犬回避ロスや疲れも相まってやはり5時間くらいかかりました。
 天気が1日もってくれたのはラッキーでした。この翌日、翌々日はいずれも午前中は快晴でも午後は雷雨となったので、この日に登っておいて良かったです。

午後になってもこの青空。天候に恵まれました

午後になってもこの青空。天候に恵まれました

 GPSのみで行った私が言うのもなんですが、ルート選択や牧羊犬・羊飼い対策、気候の変わりやすさ等を考慮すると、タバナ・ヌトレニャナ登山にはガイドを雇った方が絶対に良いです。

今回の山行ルート

今回の山行ルート。ハイキング地図(宿に置いてあるのを登山後に発見)によれば、もっと左(西)側から回り込むようなルートをとれば、標高差も少なく楽だった模様


標高グラフ

標高グラフ。距離30km超で累積標高差2000m以上・・・そりゃあ、疲れるはず!

 JICAボランティアには「私事目的任国外旅行制度」というものがあり、1年のうち20日間まで受入国以外へ旅行することが出来ます(もちろん自費です)。

 青年海外協力隊の場合は受入国毎にそれぞれ行ける国が定められていて、ボツワナの場合はケニア、マラウイ、南アフリカ、ザンビア、ジンバブエの5か国のみ。しかも、ケニアは2013年に発生したテロ事件以降渡航禁止、南アフリカも治安上の理由によりケープタウンのみ渡航可能となっているので、行ける国はかなり限られます。
 私はシニア海外ボランティアなので渡航可能国の縛りはありませんが、渡航禁止国の規定は青年海外協力隊と同じなので上述のケニアや南アフリカ(ケープタウンを除く)には行けません。

 任地のボツワナですら、まだ一部にしか行っていない状況ではありますが、せっかくアフリカに滞在しているのだから、この機会に周辺の国を訪問してみたい・・・ということでクリスマス休暇を利用して初の任国外旅行、行き先はレソト!

 レソトは周囲を南アフリカ1国に囲まれています(包領というそうです)。「南アフリカが渡航禁止なので、その中にすっぽり入るレソトもダメかな?」と思いつつもJICAに申請したところ、渡航許可が下りました。

レソトの位置

レソトの位置。周囲360度を南アフリカ1国に囲まれています


 ボツワナもレソトも同じ内陸国。出国から半年近く、海鮮料理が恋しくなるこの時期にわざわざ同じ内陸国のレソトを選んだ理由は・・・「山」です。レソトにはボツワナにはない山がある。アフリカ南部の最高峰タバナ・ヌトレニャナ山(3482m)を登るのが目的です。
 
 レソトへのアクセスですが、ボツワナからの直行便はないので、ハボロネからヨハネスブルグで乗り継いで首都マセルへ。まあ、ボツワナから他国へ渡航する場合は大概ヨハネスブルグ経由になりますから、それ自体は特に不便というわけではありません。
 ただ、タバナ・ヌトレニャナ山を目指す場合はサニパスという国境の地がベースとなるのですが、ここまで行くにはマセルからよりも南アフリカのダーバンからのアクセスの方が良いのに、「南アフリカが渡航禁止」というルールがある以上、マセル経由で行かざるを得ない・・・というのが何とももどかしい。
モショエショエ1世国際空港

マセルのモショエショエ1世国際空港。「世界で1番地味な首都の空港」と紹介しているWebサイトも・・・タクシーすらいないとの情報もありましたが、私の到着時にはシャトルタクシーが来てました(街の中心まで100マロチ=約850円)。
ちなみにレソトの通貨ロチ(複数形:マロチ)は南アフリカのランドと等価で、ランドも普通に使えます。


Maqalika Scenery Guesthouse

マセルで宿泊した宿「Maqalika Scenery Guesthouse」の部屋。1泊朝食付きで5000円くらい。スタッフがとても気さくで居心地が良く、帰りもここに泊まりました。バス・タクシー乗り場から徒歩20分ほど


 
 マセルで1泊して、翌日早朝発のモコトロン(サニパスの麓の街)行きのバスに乗ったのですが、クリスマスのせいか故郷へ帰省する乗客が多く激混み。人や荷物で通路まで完全に塞がっていて完全に過積載の状態です。
バス待ちの行列

モコトロン行きのバス。出発時刻は6時、6時半、7時など訊く人によって異なります。余裕をもって5時半に到着したのに、既に凄い列。多数の乗客と大量の荷物で搭乗に時間がかかり、結局出発したのは7時過ぎ。出発時刻の情報がまちまちだったのも、あながち間違いではないようです。


 モコトロンまでの道は「国道1号線」なのでレソトのメインルートといって良いのですが、途中3200m超の峠を越えるなど山道ばかり。「途中下り坂で制御が効かなくなったりしないだろうか?」なんて心配もしましたが、運転手さんは慎重過ぎるほど速度を落とす安全運転でした。ただし、その分所要時間は長くなりますし、途中のバス停やトイレ休憩などでも通路の荷物を全て降ろさないと乗降出来ないので、とにかく時間がかかりモコトロンまで10時間弱。その間、ろくに身動きも出来ない状態だったので疲れました。
トイレ休憩

途中、川のある所でトイレ休憩


 モコトロンからサニパスへはミニバスが走っていますが、これは朝しかないようなので当初はモコトロンに1泊する予定でした。でも天気予報を見ると翌日が一番良さそう。「それならば、この日のうちにサニパスに到着してしまおう」とタクシーを使うことにしました。交渉して6000マロチ(約5000円)、ちょっと高かったですが山登りに一番重要なのは天気・・・結果、この選択は成功でした(詳細は次回記載)。
サニパス国境ゲート

サニパス国境レソト側の出国ゲート。向こうは南アフリカ・・・ビザは不要ですが、うっかり出国してしまうとJICAにこっぴどく叱られてしまう(南アフリカは渡航禁止)ので要注意!


 宿泊先はサニパスのレソト側にある唯一(多分)の宿Sani Mountain Lodge。普通の部屋はお高いのでバックパッカー用施設のドミトリーを利用しました(1泊275マロチ=約2300円)。バックパッカー用施設はロッジとは少し離れていますが徒歩圏内。宿泊棟と共用棟の2棟構成で共用棟にはトイレ・シャワー・キッチンがあります。キッチンのコンロはガス式ですが、着火するにはマッチかライターが必要。マッチは近所のショップに売ってます。ショップでは缶詰、パスタなど最低限の食料も手に入ります。食器や鍋はありますが、備え付けの調味料はありません。冷蔵庫はありますが発電機が動いている時間(夜の数時間のみ)しか作動しません。まあ、涼しい所なので大きな問題はありませんが・・・。
Sani Mountain Lodge バックパッカー用施設

宿泊したSani Mountain Lodgeのバックパッカー用施設。左が宿泊棟、右が共用棟(トイレ・シャワー・キッチン)


ドミトリーの部屋

私の宿泊したドミトリーの1室。6人用


Sani Mountain Lodge本館

Sani Mountain Lodge本館。バックパッカー用施設とは少し離れた所にあり、当然の如くこちらの方が眺めが良い。そんなに差別化しないでバックパッカー用施設も隣に建ててくれればいいのに・・・