トップMy OpenStreetMap > 行政境界線の描画

行政境界線の描画

はじめに

OSMのレンダリングでは、陸地ポリゴン(海岸線)、広域森林、航路、高速バス路線、行政境界線など 境界ボックスが巨大になるデータの取り扱いがパフォーマンス上、極めて重要である。

例えば、県別に色分けして地図を塗りつぶすには、行政境界線をつなぎ合わせてポリゴンにする必要があるが、 ハイズーム用の詳細なデータでポリゴンを作るとそのノード(多角形の頂点)は膨大な数になり、 レンダリングには長大な時間がかかる。また、スマホではアプリが使用できるメモリがパソコンに比べて桁違いに 少ないためメモリ不足エラーになる可能性が高い。

このため、ハイズームでは、パソコン上ではポリゴンを構成するが、ポリゴンレコードとしての出力はしない。

このように、行政境界線の扱いは、他のデータとは少し異なった扱いをするため、ここに、その詳細を記載する。

リレーション処理

OSMでは色んな地物でリレーションが使われるが、パフォーマンス上注意がいるのは、広域森林、航路、高速バス路線、行政境界線など、その境界ボックスが巨大になるものである。

行政境界線は type=boundary; boundary=administrative によって識別される。 東京都と神奈川県の境界線を例にとると、境界線に沿って、東京都側に「東京都」、神奈川県側に「神奈川県」を描画する。 どちらが東京都かを判断するには、境界線をつなぎ合わせて、ポリゴンとして、ある公式で面積を算出して、その符号により、 ポリゴンが時計周りか反時計周りかを判断して、頂点の並びを反時計周りに統一する必要がある。

広域森林や水域(湖など)ではポリゴンレコードとして出力するが、東京都や神奈川県などについては、 「東京都」、「神奈川県」といった名称を描画する線分だけを断片的にラインレコードとして出力する。 レコード数としては多くなるが、この方が大きなポリゴンレコードを使って、都道府県、市区町村名などを描画するよりも、 描画時間ははるかに短くなる。

このページの主題ではないが、高速バス路線、新幹線、フェリー航路などは繋ぎ合わせて一つの ラインレコードすることはできるが、そのような境界ボックスが巨大となるラインレコードを出力してはいけない。 小さな複数のラインレコードとして出力する。同じ路線であることは osm_id で判断できる。

航路などでは、wayオブジェクト自体の境界ボックスが巨大なケースがある。 このような場合、wayオブジェクトを分割した方がパフォーマンスは向上する。 ただし、よほどの効果が見込まれない限り、そこまで踏み込むのは見合わせた方がいい。

    static void procRelation() throws Exception {
        Random random = new Random(777);
        int max = 15;
        int min = 7;
        int cntBig = 0;
        int totalNodes = 0;
        int cntRelation = 0;
        for (Relation rel : OSMParser.mapRelations.values()) {
            if (rel.tags.length == 0) continue;

            if (cntRelation++ % 1000 == 0) {
                System.out.printf("#%d %d \n", cntRelation, rel.id);
            }

            int admin_level = -1;
            HashSet<String> setTags = new HashSet<>();
            for (String tag : rel.tags) {
                setTags.add(tag);
                if (tag.startsWith("admin_level=")) {
                    admin_level = Integer.parseInt(tag.substring(12));
                    //System.out.printf("admin_level=%d\n", admin_level);
                }
            }
            if (setTags.contains("type=multipolygon") || 
                (setTags.contains("type=boundary") && !setTags.contains("boundary=civil"))) {

                boolean isBoundary = setTags.contains("type=boundary");     // 2022.5.19
                boolean isAdministrative = setTags.contains("boundary=administrative");

                List<long[]> listOuter = new ArrayList<>();
                List<long[]> listInner = new ArrayList<>();
                List<long[]> listPendingOuter = new ArrayList<>();
                List<long[]> listPendingInner = new ArrayList<>();

                for (Member m : rel.members) {
                    if (m.type.equals("way")) {
                        if (!OSMParser.mapWays.containsKey(m.ref_id)) {
                            System.out.println("no way #" + m.ref_id);
                            continue;
                        }
                        long[] lonlats = OSMParser.mapWays.get(m.ref_id); // way lonlats
                        if (lonlats == null || lonlats.length == 0) {
                            continue;
                        }
                        if (lonlats[0] == lonlats[lonlats.length-1]) { // 閉ループか?
                            if (m.role.equals("outer")) listOuter.add(lonlats);
                            else if (m.role.equals("inner")) listInner.add(lonlats);
                        } else {
                            if (m.role.equals("outer")) listPendingOuter.add(lonlats);
                            else if (m.role.equals("inner")) {
                                listPendingInner.add(lonlats);
                            }
                        }
                    }
                }

                Concat(listOuter, listPendingOuter, rel.id); // Outer
                if (listPendingOuter.size() > 0) {
                    System.out.printf("%d: Outer=%d, Pending=%d\n", rel.id, listOuter.size(), listPendingOuter.size());
                }
                Concat(listInner, listPendingInner, rel.id);  // Inner
                if (listPendingInner.size() > 0) {
                    System.out.printf("%d: Inner=%d, Pending=%d\n", rel.id, listInner.size(), listPendingInner.size());
                }

                // Outer の後に所属の Inner を見つけて後ろに繋ぐ
                for (long[] outer : listOuter) {
                    double way_area = OSMLib.calcWayArea(outer);        // 符号付き
                    if (way_area < 0) OSM.reverse(outer);           // 反時計回りに変換する
                    List<long[]> listIn = new ArrayList<>();
                    int totalLength = outer.length;
                    for (long[] inner : listInner) {
                        if (OSMParser.outLow && Math.abs(OSMLib.calcWayArea(inner)) < 1000) {
                            continue;   // 小さな inner polygon は無視する
                        }
                        if (OSMLib.PointInPolygon(inner[0], outer)) {
                            listIn.add(inner);
                            totalLength += inner.length;
                        }
                    }
                    if (outer[0] != outer[outer.length-1]) {
                        System.out.printf("error: outer polygon isn't closed!! rel.id=%d\n", rel.id);
                    }

                    long[] newOuter = outer;
                    int[] multi = null;
                    if (listIn.size() > 0) {
                        multi = new int[1 + listIn.size()];
                        multi[0] = outer.length;
                        for (int i = 0; i < listIn.size(); i++) {
                            long[] in = listIn.get(i);
                            multi[1+i] = in.length;
                        }
                        newOuter = new long[totalLength];
                        System.arraycopy(outer, 0, newOuter, 0, outer.length);
                        int next = outer.length;
                        for (long[] in : listIn) {
                            System.arraycopy(in, 0, newOuter, next, in.length);
                            next += in.length;
                            if (in[0] != in[in.length-1]) {
                                System.out.printf("error: inner polygon isn't closed!! rel.id=%d\n", rel.id);
                            }
                        }
                    }

                    if (totalLength != newOuter.length) {
                        System.out.printf("error: rel.id=%d  totalLength=%d, newOuter.length=%d\n",
                                   rel.id, totalLength, newOuter.length);
                    }

                    if (isBoundary) {
                        if (!OSMParser.outLow && isAdministrative) {
                            // 境界線に沿って行政境界名をかくためのデータ出力
                            for (int i = 0; i+1 < outer.length; ) {
                                long[] lonlats = new long[2];
                                lonlats[0] = outer[i];
                                lonlats[1] = outer[i+1];
                                OSM osm = new OSM(1, -rel.id, null, lonlats, rel.tags);
                                OSMParser.dos.write(osm.toBinaryRecord());//writeOSM(osm);
                                i += random.nextInt(max - min) + min;
                            }
                        }
                        if (!isAdministrative) {
                            int type = multi==null ? 2 : 3;
                            OSM osm = new OSM(type, -rel.id, multi, newOuter, rel.tags);
                            OSMParser.dos.write(osm.toBinaryRecord());//writeOSM(osm);  // boundary=national_parkなど
                            //System.out.printf("%d: type=%d  %s\n", rel.id, type, "administrative以外");
                        }
                    } else {
                        int type = multi==null ? 2 : 3;
                        //System.out.printf("%d: type=%d  inner = %d\n", rel.id, type, listIn.size());
                        OSM osm = new OSM(type, -rel.id, multi, newOuter, rel.tags);
                        OSMParser.dos.write(osm.toBinaryRecord());//writeOSM(osm);
                    }
                }

            } else if (setTags.contains("type=route") && setTags.contains("route=bus")
                             && !OSMParser.outMid && !OSMParser.outLow) {
                for (Member m : rel.members) {
                    if (m.type.equals("way")) {
                        if (!OSMParser.mapWays.containsKey(m.ref_id)) {
                            //System.out.printf("rel #d: no way #%d\n", rel.id, m.ref_id);
                            continue;
                        }
                        long[] lonlats = OSMParser.mapWays.get(m.ref_id); // way lonlats
                        if (lonlats == null || lonlats.length == 0) {
                            //System.out.printf("rel %d: no nodes #%d\n", rel.id, m.ref_id);
                            continue;
                        }
                        OSM osm = new OSM(1, -rel.id, null, lonlats, rel.tags);
                        OSMParser.dos.write(osm.toBinaryRecord()); // relationレコード出力
                        //writeOSM(osm);
                    } else if (m.type.equals("node")) {     // 2022.5.6 バス路線に登録されたバス停
                        long lonlat = NodeBlock.getLonLat(m.ref_id);
                        if (lonlat > Long.MIN_VALUE) {
                            OSM osm = new OSM(0, -rel.id, null, new long[] { lonlat }, rel.tags);
                            OSMParser.dos.write(osm.toBinaryRecord());//writeOSM(osm);   // nodeレコードを出力
                        }   // 例えば kanto.osm には千葉-京都間高速バスのバス停は関東エリア分しかない。
                    }
                }
            }
        }
    }

行政境界線描画規則

行政境界線の描画規則は簡単である。主に破線を描画する。都道府県境のみ実線に変更した。

    public RenderAdmin() {
        //Paint paAdmin3_4 = Renderer.getPaint4Line(0x80ac46ac, 4, new float[]{7, 5});
        paAdmin3_4 = getPaintLine(0xc0ac46ac, 1.5f, null, 0);
        paAdmin5 = getPaintLine(0xffac46ac, 2, new float[]{6,3,2,3,2,3}, 0);
        paAdmin6 = getPaintLine(0xffac46ac, 1.5f, new float[]{6,3,2,3,2,3}, 0);
        paAdmin7_8 = getPaintLine(0xffac46ac, 1.2f, new float[]{6,3,2,3,2,3}, 0);
        paAdmin9_10 = getPaintLine(0xffac46ac, 1, new float[]{6,3,2,3,2,3}, 0);
        paintAdminText = getPaint(0xffac46ac, 12);
    }

    void render(Canvas cv, TileToRender tile, OSM[] osms, int fromIndex, int toIndex) {
        //========= 行政境界線(boundary=administrative) ========//
        final int zoom = tile.zoom;
        for (int n = fromIndex; n < toIndex; n++) {
            OSM osm = osms[n];
            if (osm.osm_id() < 0 || (osm.tag_flags&bitBoundary) == 0) continue;

            int admin_level = osm.getNumber(Tag.Key.admin_level);

            if (admin_level <= 4) {
                osm.drawLongLine(cv, paAdmin3_4, tile);
            } else if (admin_level == 5) {
                if (zoom >= 8) osm.drawLongLine(cv, paAdmin5, tile);
            } else if (admin_level == 6) {
                if (zoom >= 12) osm.drawLongLine(cv, paAdmin6, tile);
            } else if (admin_level == 7 || admin_level == 8) {
                if (zoom >= 14) osm.drawLongLine(cv, paAdmin7_8, tile);
            } else if (admin_level == 9 || admin_level == 10) {
                if (zoom >= 16) osm.drawLongLine(cv, paAdmin9_10, tile);
            }
        }
    }

境界線の描画メソッド drawLongLine

    final void drawLongLine(Canvas canvas, Paint paint, TileToRender tile) {
        drawLongLine(canvas, paint, tile, bgnData, nodes);
    }

    void drawLongLine(Canvas canvas, Paint paint, TileToRender tile, int off, int len) {
        if (len <= 0) return;
        double fact = tile.fact;
        double xpx = tile.xpx;       // 画素単位
        double ypx = tile.ypx;
        float px=Float.MIN_VALUE, py=Float.MIN_VALUE;
        int INC = slim ? 1 : 2;
        int end = off + len * INC;
        float fx, fy;
        for (int ix = off; ix < end; ) {
            if (slim) {
                int xy = buf[ix++];
                int dx = (short)(xy >> 16);
                int dy = (short)(xy&0xffff);
                fx = (float) ((x0 + dx) * fact - xpx);
                fy = (float) ((y0 + dy) * fact - ypx);
            } else {
                fx = (float) (buf[ix++] * fact - xpx);
                fy = (float) (buf[ix++] * fact - ypx);
            }
            if (ix > off+INC) {
                canvas.drawLine(px, py, fx, fy, paint);
            }
            px = fx;
            py = fy;
        }
    }

zoom 6の画面を下に示す。東京都の島嶼部が鎖線で描画されているところがある。 現在は wayオブジェクトで描画している影響であろう。 relation処理で境界線の admin_level の値を決めれば解決する。