Android Google Maps API中的数千个多边形会增加主线程

时间:2017-09-24 16:55:20

标签: android api maps polygons

我正在开发一个应用程序,用于从文本文件中的给定GPS位置绘制移动路径。 到目前为止,我已成功加载数据并绘制路径。 我的解决方案是,路径必须绘制为矩形,因为它包含数据(例如颜色和宽度)和可点击。一条线无法做到这一点。此外,这些矩形在转弯时也存在三角形间隙,所以我用三角形填充它。并且路径必须是双倍的,因为我有另一个数据要显示在第二层上。这意味着,要为更长的路径绘制许多多边形。

我在15公里的路程上试过这个,需要1000行gps数据。 当我解析文件时,它为两个图层总共绘制了多达5000个多边形。

我的问题是,当它绘制多达1000个多边形的形状时,应用程序变得迟钝,没有响应。如果我让线程休眠1秒,似乎没问题。但速度越来越快,它没有响应。

我一直在网上阅读这个解决方案并且可以找到它。 Fyi,我已经创建了另一个线程来处理文本文件。 我还通过让app在不绘制多边形的情况下完成整个过程来缩小问题范围,并且过程非常顺利。 我已经读过没有其他方法可以处理主线程之外的多边形。

更新: 我正在使用Asynctask background从textfile中读取一行,将其解析为包含Latitude,Longitude,Value1,Value2的数组。 然后在那里进行大量计算。 完成每一行后,我将对象发送到onProgressUpdate以更新带有标记,折线和形状的UI线程。

这是我的Asynctask

private class DrawPathAsync extends AsyncTask<File, Object, Void>
{

    FileInputStream is;
    BufferedReader reader;



    @Override
    protected Void doInBackground(File... params) {
        File sFile = params[0];
        Integer count;
        String line = "";
        double radius = 8; //8 meter
        double distance;

        double Heading_y;
        int kaler, gkaler;
        double apprate, gi;

        if (sFile.exists()) {
            try {
                is = new FileInputStream(sFile);
                reader = new BufferedReader(new InputStreamReader(is));
                reader.readLine(); // this will read the first line


                while ((line = reader.readLine()) != null) {

                    String[] valuesArray = line.split("\\s*,\\s*");
                    Float bearing = (float) 0;

                    Double lat = Double.parseDouble(valuesArray[1]);
                    Double lng = Double.parseDouble(valuesArray[2]);

                    LatLng latlng = new LatLng(lat, lng);
                    LatLng center = latlng;
                    apprate = Double.parseDouble(valuesArray[3]); 

                    if (apprate >=0 && apprate < 80) {
                        kaler = appcolor1; 
                    } else if (apprate >=80 && apprate < 100) { 
                        kaler = appcolor2;
                    } else if (apprate >=100 && apprate < 120) {  
                        kaler = appcolor3;
                    } else if (apprate >=120 && apprate < 140) {  
                        kaler = appcolor4;
                    } else if (apprate >=140 && apprate < 160) {  
                        kaler = appcolor5;
                    } else if (apprate >=160 && apprate <= 200) {  
                        kaler = appcolor6;
                    } else {
                        kaler = appcolor7; 
                    }


                    if (points.size()== 2) {
                        points.remove(0);
                        points.add(latlng);
                    } else {
                        points.add(latlng);
                    }


                    //recheck
                    if (points.size() == 2) {

                        distance = SphericalUtil.computeDistanceBetween(center, points.get(0));

                        LatLng pt1 = points.get(0);
                        LatLng pt2 = latlng;

                        bearing = (float) SphericalUtil.computeHeading(pt1, pt2);
                        if (bearing < 0) {
                            bearing = bearing + 360;
                        }

                        LatLng x = SphericalUtil.computeOffset(center, radius, bearing - 90);
                        LatLng y = SphericalUtil.computeOffset(center, radius, bearing + 90);


                        LatLng a = SphericalUtil.computeOffset(x, distance, bearing + 180);
                        LatLng b = SphericalUtil.computeOffset(y, distance, bearing + 180);


                        MarkerPoint mp = new MarkerPoint();
                        mp.latlng = latlng;
                        mp.bearing = bearing;

                        Rect rc = new Rect();
                        rc.a = a;
                        rc.b = b;
                        rc.x = x;
                        rc.y = y;
                        rc.kaler = kaler;
                        rc.pt2 = pt2;

                        publishProgress(mp, rc);
                    }

                    Thread.sleep(50);

                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }

        return null;
    }

    @Override
    protected void onProgressUpdate(Object... values) {

        MarkerPoint mp = (MarkerPoint) values[0];
        Rect rc = (Rect) values[1];



        LatLng latlng = mp.latlng;

        BitmapDescriptor icon = BitmapDescriptorFactory.fromResource(R.mipmap.pointer);
        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(latlng);
        markerOptions.icon(icon);
        markerOptions.rotation(mp.bearing);

        mMap.moveCamera(CameraUpdateFactory.newLatLng(latlng));
        marker1.remove();
        marker1 = mMap.addMarker(markerOptions);

        if (points.size() > 1) {
            path = mMap.addPolyline(new PolylineOptions().add(points.get(0)).add(points.get(1)).color(Color.BLUE).width(5));
            lines.add(path);
        }

        PolygonOptions options = new PolygonOptions()
                .fillColor(rc.kaler)
                .strokeWidth(0)
                .strokeColor(Color.TRANSPARENT);
        options.add(rc.x);
        options.add(rc.y);
        options.add(rc.b);
        options.add(rc.a);

        rect = mMap.addPolygon(options);
        rects.add(rect);

        if (tripoints.size() == 3) {
            tripoints.add(rc.a);
            tripoints.add(rc.b);
        } else {



            tripoints.add(rc.pt2);
            tripoints.add(rc.x);
            tripoints.add(rc.y);
        }

        //check
        //round 2, if triponts = 5 create triangle
        if (tripoints.size() == 5) {
            PolygonOptions options2 = new PolygonOptions()
                    .fillColor(rc.kaler)
                    .strokeWidth(0)
                    .strokeColor(Color.TRANSPARENT);

            options2.add(tripoints.get(0));
            options2.add(tripoints.get(2));
            options2.add(tripoints.get(4));


            t1 = mMap.addPolygon(options2);
            tris.add(t1);


            PolygonOptions options3 = new PolygonOptions()
                    .fillColor(rc.kaler)
                    .strokeWidth(0)
                    .strokeColor(Color.TRANSPARENT);

            options3.add(tripoints.get(0));
            options3.add(tripoints.get(1));
            options3.add(tripoints.get(3));



            t2 = mMap.addPolygon(options3);
            tris.add(t2);



            tripoints.clear();
            tripoints.add(rc.pt2);
            tripoints.add(rc.x);
            tripoints.add(rc.y);
        }


    }

    @Override
    protected void onPostExecute(Void result) {

    }
}

希望有人可以分享一些提示和解决方案。

3 个答案:

答案 0 :(得分:2)

一段时间以来,我一直遇到相同的问题,经过广泛的研究,我将问题隔离到Google Maps SDK本身。在我的情况下,一种有效的解决方案是使用GroundOverlay并在正确的地理坐标处绘制自定义点/线/多边形。我发现这个库基本上是这样做的:

https://github.com/antoniocarlon/richmaps

通过一些调整,我能够创建一个渲染线程,该线程将绘图/过滤部分从主UI线程中删除,并仅在完成时更新GroundOverlay。另外,我添加了一个简单但快速的算法来搜索当前可见的形状。通过这种方法,您可以获得一些好处:

  • 仅绘制当前视口内的对象(Google Maps SDK会绘制此对象,但彼此之间距离不太近的形状(例如在600+的情况下)不会绘制)
  • 绘图是在线程中完成的,因此可以极大地改善应用程序的启动
  • 您可以将其他自定义选项添加到地图形状中,如下所示: 绘图是通过画布完成的
  • 在我的情况下,使用600多种stapes可以将地图渲染时间减少到大约200ms。

答案 1 :(得分:0)

我知道现在回答这个问题为时已晚,但我写出了我的解决方案。我希望这会有所帮助。 我创建了一个“PolygonRenderer”类,它不断更新地图上的多边形。 我还创建了一个“PolygonWrapper”类,其中包含有关多边形的更多信息,例如是否将多边形添加到地图中,以及更多功能,如 removeFromMap() 和 addToMap()

实际上我使多边形延迟加载,并且只会显示视口中的多边形而其他多边形将被删除。通过改变地图相机的位置,可见的多边形将会改变。

PolygonRenderer 类:

/**
 * Singleton class that handle polygons showing on map.
 * This class is singleton because one instance in enough and operation that this class do it is
 * UI Thread operation so must call in ui thread.
 * This class only show polygons that are in map viewport and handle shapes.
 *
 * @version 1.3
 */
public class PolygonRenderer implements Runnable {

    // Update interval in millisecond
    private final static long UPDATE_INTERVAL = 500;

    // Single instance of this class
    private static PolygonRenderer instance;

    private Thread thread;

    // Keep last update time in millisecond
    private long lastUpdate;

    // Used to stop thread
    private boolean stopFlag;

    // Used to pause thread
    private boolean pauseFlag;
    private final Object pauseLock;

    private LatLngBounds bounds;
    private float zoom;
    private List<PolygonWrapper> polygons;
    private GoogleMap map;

    private PolygonRenderer() {
        this.stopFlag = false;
        this.pauseFlag = false;
        this.pauseLock = new Object();
    }

    public static synchronized PolygonRenderer getInstance() {
        if (instance == null)
            instance = new PolygonRenderer();
        return instance;
    }

    /**
     * Stop polygons refreshing on map
     */
    public synchronized void stop() {
        stopFlag = true;
        if (thread != null) {
            thread.interrupt();
            thread = null;
        }
    }

    /**
     * Pause running thread
     */
    public synchronized void onPause() {
        pauseFlag = true;
    }

    /**
     * Resume thread running
     */
    public synchronized void onResume() {
        pauseFlag = false;
        pauseLock.notifyAll();
    }

    /**
     * Create new polygon wrapper and add it to polygons list.
     *
     * @param activity context activity of map
     * @param id       id of polygon
     * @param geometry data of polygon such as points
     * @param polygons list af all polygons
     * @see PolygonWrapper for more info about polygon wrapper.
     */
    public synchronized void createPolygons(Activity activity, String id, String geometry, List<PolygonWrapper> polygons) {
        try {

            // Read polygon data (coordinates)
            WKTReader wkt = new WKTReader();
            if (geometry.contains("MULTIPOLYGON")) {
                org.locationtech.jts.geom.MultiPolygon multiPolygon = (org.locationtech.jts.geom.MultiPolygon) wkt.read(geometry);

                // Gets each polygon of a multipolygon
                for(int i = 0; i < multiPolygon.getNumGeometries(); i++) {

                    org.locationtech.jts.geom.Polygon polygon = (org.locationtech.jts.geom.Polygon) multiPolygon.getGeometryN(i);

                    // Create polygon options
                    PolygonOptions options = new PolygonOptions();
                    options.strokeWidth(8);
                    options.clickable(true);

                    // Gets each polygon outer coordinates
                    ArrayList<LatLng> outer = new ArrayList<>();
                    Coordinate[] outerCoordinates = polygon.getExteriorRing().getCoordinates();
                    for (Coordinate outerCoordinate : outerCoordinates)
                        outer.add(new LatLng(outerCoordinate.y, outerCoordinate.x));
                    options.addAll(outer);

                    // Getting each polygon interior coordinates (hole) if they exist
                    if(polygon.getNumInteriorRing() > 0){
                        for(int j = 0; j < polygon.getNumInteriorRing(); j++){
                            ArrayList<LatLng> inner = new ArrayList<>();
                            Coordinate[] innerCoordinates = polygon.getInteriorRingN(j).getCoordinates();
                            for (Coordinate innerCoordinate : innerCoordinates)
                                inner.add(new LatLng(innerCoordinate.y, innerCoordinate.x));
                            options.addHole(inner);
                        }
                    }

                    // Create and add polygon wrapper
                    polygons.add(new PolygonWrapper(activity, id, options, PolygonWrapper.Behavior.PART_SHOWING));
                }
            } else {
                org.locationtech.jts.geom.Polygon polygon = (org.locationtech.jts.geom.Polygon) wkt.read(geometry);

                // Create polygon options
                PolygonOptions options = new PolygonOptions();
                options.strokeWidth(8);
                options.clickable(true);

                // Gets polygon outer coordinates
                ArrayList<LatLng> outer = new ArrayList<>();
                Coordinate[] outerCoordinates = polygon.getExteriorRing().getCoordinates();
                for (Coordinate outerCoordinate : outerCoordinates)
                    outer.add(new LatLng(outerCoordinate.y, outerCoordinate.x));
                options.addAll(outer);

                // Getting polygon interior coordinates (hole) if they exist
                if(polygon.getNumInteriorRing() > 0){
                    for(int j = 0; j < polygon.getNumInteriorRing(); j++){
                        ArrayList<LatLng> inner = new ArrayList<>();
                        Coordinate[] innerCoordinates = polygon.getInteriorRingN(j).getCoordinates();
                        for (Coordinate innerCoordinate : innerCoordinates)
                            inner.add(new LatLng(innerCoordinate.y, innerCoordinate.x));
                        options.addHole(inner);
                    }
                }

                // Create and add polygon wrapper
                polygons.add(new PolygonWrapper(activity, id, options, PolygonWrapper.Behavior.PART_SHOWING));
            }

        } catch (org.locationtech.jts.io.ParseException e) {
            e.printStackTrace();
        }
    }

    /**
     * Update visible polygons on map based on locating in map viewport.
     * Also map zoom is important in showing polygons, because of polygons count on map in low zooms.
     * We remove some very small polygons in low zoom.
     * This operations is require to prevent app not responding when polygons are too many.
     * Polygons that are not in viewport will be remove from map.
     * This method must be call in onCameraMove event to get map new bounds and zoom.
     * Operations will be done in new thread. Thread change polygons visibility continuously.
     *
     * @param map      map that polygons must be shown on it
     * @param polygons list of all polygons
     */
    public synchronized void updatePolygons(GoogleMap map, List<PolygonWrapper> polygons) {

        // Limit update interval
        long time = SystemClock.elapsedRealtime();
        if (time - lastUpdate < UPDATE_INTERVAL)
            return;

        // Update last update time
        lastUpdate = time;

        // Bounds and zoom should get in ui thread. so we get them out of thread
        this.bounds = map.getProjection().getVisibleRegion().latLngBounds;
        this.zoom = map.getCameraPosition().zoom;

        // We have only one thread and if it is created so we don't need recreate it
        if (thread != null)
            return;

        // Create and run thread
        this.map = map;
        this.polygons = polygons;
        this.stopFlag = false;
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        while (!stopFlag) {

            // Call by try-catch to prevent unwanted exception and thread stopping
            try {

                // Pause implementation
                synchronized (pauseLock) {
                    while (pauseFlag) {
                        try {
                            pauseLock.wait();
                        } catch (InterruptedException ignored) {
                        }
                    }
                }

                // Update visible polygons on map based on map viewport
                for (PolygonWrapper polygon : polygons) {

                    // Remove polygons that are invisible in given zoom from map
                    if (isVisibleWithZoom(polygon, zoom)) {
                        if (polygon.isAddedToMap()) {
                            polygon.removeFromMap();
                            sleep();
                        }
                        continue;
                    }

                    // Hide out of map viewport polygons
                    if (polygon.isWithin(bounds) && !polygon.isAddedToMap()) {
                        polygon.addToMap(map);
                        sleep();
                    } else if (!polygon.isWithin(bounds)) {
                        polygon.removeFromMap();
                        sleep();
                    }
                }
            } catch (Exception ignored) {
            }
        }
    }

    private boolean isVisibleWithZoom(PolygonWrapper polygon, float zoom) {

        // Compute area of polygon
        double area = SphericalUtil.computeArea(polygon.getOptions().getPoints());

        return (
                (zoom <= 11 && area <= 1000) ||                         // Don't show polygons with area <= 1000 when zoom is <= 11 (map bounds has great area)
                        (zoom > 11 && zoom <= 12 && area <= 500) ||     // Don't show polygons with area <= 500 when zoom is between 11 and 12
                        (zoom > 12 && zoom <= 13 && area <= 250) ||     // Don't show polygons with area <= 250 when zoom is between 12 and 13
                        (zoom > 13 && zoom <= 13.5 && area <= 200) ||   // Don't show polygons with area <= 200 when zoom is between 13 and 13.5
                        (zoom > 13.5 && zoom <= 14 && area <= 150) ||   // Don't show polygons with area <= 150 when zoom is between 13.5 and 14 (map bounds has small area)
                        (zoom > 14 && zoom <= 14.5 && area <= 100)      // Don't show polygons with area <= 100 when zoom is between 14 and 14.5 (map bounds has small area)
        );
    }

    /**
     * Thread sleep allow ui thread to show views and doesn't hang up.
     * Call this method everywhere ui thread action is performing.
     */
    private void sleep() throws InterruptedException {
        Thread.sleep(8);
    }
}

和 PolygonWrapper 类:

/**
 * Wrapper class for polygon.
 * See https://stackoverflow.com/questions/36439031/determine-if-polygon-is-within-map-bounds for more info.
 *
 * @version 1.1
 * */
public class PolygonWrapper {

    private final String id;
    private final Behavior behavior;
    private final LatLng northWest, northEast, southEast, southWest;
    private final Activity activity;

    private Polygon polygon;
    private PolygonOptions options;

    public void addToMap(GoogleMap map) {
        activity.runOnUiThread(() -> {
            if (isAddedToMap()) removeFromMap();
            polygon = map.addPolygon(options);
        });
    }

    public void removeFromMap() {
        activity.runOnUiThread(() -> {
            if (isAddedToMap()) {
                polygon.remove();
                polygon = null;
            }
        });
    }

    public PolygonWrapper(Activity activity, String id, PolygonOptions options, Behavior behavior) {
        this.activity = activity;
        this.id = id;
        this.options = options;
        this.behavior = behavior;

        Double north = null, west = null, south = null, east = null;
        for (LatLng latLng : options.getPoints()) {
            if (north == null || latLng.latitude > north)
                north = latLng.latitude;

            if (west == null || latLng.longitude < west)
                west = latLng.longitude;

            if (south == null || latLng.latitude < south)
                south = latLng.latitude;

            if (east == null || latLng.longitude > east)
                east = latLng.longitude;
        }
        northWest = new LatLng(north, west);
        northEast = new LatLng(north, east);
        southEast = new LatLng(south, east);
        southWest = new LatLng(south, west);
    }

    public String getId() {
        return id;
    }

    public PolygonOptions getOptions() {
        return options;
    }

    public Polygon getPolygon() {
        return polygon;
    }

    public PolygonOptions buildBordersRectPolygonOptions() {
        final PolygonOptions rvalue = new PolygonOptions();
        rvalue.add(northWest);
        rvalue.add(northEast);
        rvalue.add(southEast);
        rvalue.add(southWest);
        rvalue.add(northWest);
        rvalue.fillColor(0x6A00FFFF);
        rvalue.strokeColor(0x6AFF0000);
        rvalue.strokeWidth(1f);
        return rvalue;
    }

    public boolean isWithin(LatLngBounds bounds) {
        boolean within = false;
        switch (behavior) {
            case FULL_SHOWING:
                if (bounds.contains(northWest) && bounds.contains(southEast))
                    within = true;
                break;
            case PART_SHOWING:
                if (bounds.contains(northWest)
                        || bounds.contains(southEast)
                        || bounds.contains(northEast)
                        || bounds.contains(southWest)) {
                    within = true;
                } else if (northEast.latitude > bounds.southwest.latitude
                        && northEast.longitude > bounds.southwest.longitude
                        && southWest.latitude < bounds.northeast.latitude
                        && southWest.longitude < bounds.northeast.longitude) {
                    within = true;
                }
                break;
        }
        return within;
    }

    public boolean isAddedToMap() {
        return polygon != null;
    }

    public enum Behavior {
        FULL_SHOWING, PART_SHOWING
    }
}

我们必须在相机位置变化时更新多边形:

map.setOnCameraMoveListener(() -> {
     // Update visible polygons on map
     PolygonRenderer.getInstance().updatePolygons(map, polygons);
});

我们还需要这样做:

@Override
protected void onStop() {
    // Stop polygons renderer class on activity stop
    PolygonRenderer.getInstance().stop();

    super.onStop();
}

在这段代码中,我也通过缩放更改来更改多边形的可见性。但如果多边形具有比例并且适用于不同的缩放级别,则此选项会更好。

我还使用了 jts-core 来获取多边形数据。

通过这种方式,我在地图上处理了大约 2000-4000 个多边形。

答案 2 :(得分:0)

与 Polygons 相比,GroundOverlay 显示出显着的性能提升,但在生产方面仍然滞后。

TileOverlay 是正确的解决方案。很难找到,但是一旦你到了那里,the homepage of TileOverlay docs 提到的很清楚

<块引用>

当您想将大量图像添加到 地图,通常覆盖较大的地理区域。相比之下,地 当您希望将单个图像固定在某个区域时,叠加层很有用 地图。

虽然the homepage of GroundOverlays documentations说同样的话

<块引用>

如果您想添加涵盖大部分内容的广泛图像 地图,您应该考虑使用 Tile 叠加层。

令人遗憾的是,Maps Polygon Android 文档没有提及此类建议,因此这是一个鸡和蛋的问题。请注意,数以千计的多边形在 iOS 上运行良好!