maRci002 / proj4dart

Proj4dart is a Dart library to transform point coordinates from one coordinate system to another, including datum transformations.
MIT License
26 stars 2 forks source link

flutter_map - Proj4Crs (Custom CRS) not works #2

Closed moovida closed 4 years ago

moovida commented 4 years ago

Even if this is happening in a flutter_map project, I feel that it makes sense to post the issue here.

I am trying to load a WMS layer with a defined CRS. What I am doing is more or less:

    final resolutions = <double>[
      134217728, // 20
      67108864,
      33554432,
      16777216,
      8388608,
      4194304, //15
      2097152,
      1048576,
      524288,
      262144,
      131072, // 10
      65536,
      32768, 
      16384,
      8192,
      4096, // 5
      2048,
      1024,
      512,
      256, 
      128, // 0
    ];
    var crs = Proj4Crs.fromFactory(
        code: 'EPSG:3857',
        proj4Projection: projection3857,
        resolutions: resolutions);

    List<LayerOptions> layers = [
      TileLayerOptions(
        opacity: 1,
        backgroundColor: Colors.transparent,
        wmsOptions: WMSTileLayerOptions(
          crs: crs,
          transparent: true,
          format: imageFormat,
          baseUrl: _getCapabilitiesUrl,
          layers: [_layerName],
        ),
      )
    ];

But on creation of the layer, I am getting:

The setter 'z=' was called on null.
Receiver: null
Tried calling: z=null

with the following stacktrace:

The relevant error-causing widget was
    FlutterMap 
lib/…/core/onlinesourcespage.dart:216
When the exception was thrown, this was the stack
#0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:53:5)
#1      Projection.transform package:proj4dart/…/classes/projection.dart:171
#2      _Proj4Projection.project package:flutter_map/…/crs/crs.dart:452
#3      WMSTileLayerOptions.getUrl package:flutter_map/…/layer/tile_layer.dart:262
#4      TileProvider.getTileUrl package:flutter_map/…/tile_provider/tile_provider.dart:20
...

Am I missing something in the creation?

Just as info, if it helps:

On a sidenote: when removing the crs option it works.

maRci002 commented 4 years ago

On a sidenote: when removing the crs option it works.

That's because WMSTileLayerOptions using Epsg3857 by default see: this.crs = const Epsg3857(), and that just works fine.

The main problem is the provided resolutions in your code. You can create a little helper function which will resolve the resolutions like this:

List<double> getResolutions(double maxX, double minX, int zoom,
    [double tileSize = 256.0]) {
  var size = (maxX - minX) / tileSize;

  return List.generate(zoom, (z) => size / math.pow(2, z));
}

Usually you can get the maxx / minx from the Capabilities http://geoservices.retecivica.bz.it/geoserver/gwc/service/wmts?request=GetCapabilities Then you should find something like this:

      <Layer queryable="1">
        <Name>iier:EK_FA_BOKORCS</Name>
        <Title>EK_FA_BOKORCS</Title>
        <Abstract/>
        <SRS>EPSG:23700</SRS>
        <BoundingBox SRS="EPSG:23700" minx="420000.0" miny="40000.0" maxx="944288.0" maxy="564288.0"/>
      </Layer>

However I couldn't find it. You can get extent from https://epsg.io/ also see https://epsg.io/3857.

Projected bounds:
-20026376.39 -20048966.10
20026376.39 20048966.10

Warning above bounds misleading see the first comment:

The Projected bounds should be wrong.
X domain must be (6378137 * Pi) = 20037508.34

So the right bounds would be:

Projected bounds:
-20037508.342789244 -20037508.342789244
20037508.342789244 20037508.342789244

Here is the final code (I rewrote wms_tile_layer.dart in flutter_map):

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;

import '../widgets/drawer.dart';

List<double> getResolutions(double maxX, double minX, int zoom,
    [double tileSize = 256.0]) {
  var size = (maxX - minX) / tileSize;

  return List.generate(zoom, (z) => size / math.pow(2, z));
}

int r = 6378137;
double _boundsD = r * math.pi;
final Bounds<double> _bounds = Bounds<double>(
  CustomPoint<double>(-_boundsD, -_boundsD),
  CustomPoint<double>(_boundsD, _boundsD),
);

final resolutions = getResolutions(_boundsD, -_boundsD, 19);
final maxZoom = (resolutions.length - 1).toDouble();

final epsg3857CRS = Proj4Crs.fromFactory(
  code: 'EPSG:3857',
  proj4Projection: proj4.Projection('EPSG:3857'),
  resolutions: resolutions,
  bounds: _bounds,
);

class WMSLayerPage extends StatelessWidget {
  static const String route = 'WMS layer';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('WMS Layer')),
      drawer: buildDrawer(context, route),
      body: Padding(
        padding: EdgeInsets.all(8.0),
        child: Column(
          children: [
            Padding(
              padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
              child:
                  Text('This is a map that is showing (46.762784, 11.450838)'),
            ),
            Flexible(
              child: FlutterMap(
                options: MapOptions(
                  center: LatLng(46.762784, 11.450838),
                  zoom: 17,
                  maxZoom: maxZoom,
                  crs: epsg3857CRS,
                  onTap: print,
                ),
                layers: [
                  TileLayerOptions(
                    wmsOptions: WMSTileLayerOptions(
                      crs: epsg3857CRS,
                      baseUrl:
                          'http://geoservices.retecivica.bz.it/geoserver/ows?',
                      layers: ['P_BZ_BASEMAP_HYBRID_EPSG3857'],
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
maRci002 commented 4 years ago

Maybe in Flutter Map documentation should mention this, however neither Proj4Leaflet nor OpenLayers mention it.

@moovida please let me know if above code works if so I am going to close this issue.

moovida commented 4 years ago

Hi @maRci002 , now I see the point, thanks for the description.

So in fact the getCapabilites needs to be parsed by the enduser library, right? I will have a look at it. I would love to add custom map view to SMASH ( https://www.geopaparazzi.org/smash/index.html ) but I can't ask a user to define bounds. So this has to be extracted from the getCapabilities, but as you are writing, there might be errors in that.

I will have to do some test. I will then report back here to let you know if this works well for other CRS as well.

maRci002 commented 4 years ago

If you really need to parse getCapabilites then you could use something like this (I know it's wmts but here they calculate bounds which will have maxX, minX): http://www.atlefren.net/post/2014/05/how-to-calculate-maxresolution-for-wmts-given-info-in-getcapabilities/

Search a little bit on Google how to calculate resolutions in general.

moovida commented 4 years ago

Thanks @maRci002 , I am trying to wrap my head around this and I am not sure I understand one thing. What you write is ok to define resolutions for a given WMS and its projection. And one could go over to epsg.io or spatialreference.org to get the CRS bounds, even if in the past those have proved to be not always the right ones.

But what I want to achieve is to have a map view with a CRS for which I can:

For this I can't use a resolution schema based on the data I am loading, but it needs to be on the projection, right? Is there a way for me to get to this? I mean, I can live with the fact that it doesn't load raster data of a different CRS, but the rest should load.

maRci002 commented 4 years ago

Maybe @fegyi001 could help because I'm not a GIS expert.

I think the getResolutions helper method works only for EPSG:3857. This could help also.

moovida commented 4 years ago

Thanks @maRci002 , thank you. Indeed, mine would be a requirement similar to that of a GIS map view. Looking forward to see if that is even feasible.

Just on a sidenote, even if that does not work, please know that I highly appreciate this project. Being able to reproject vector data is already a dream come true. :-)

moovida commented 4 years ago

BTW, do you know of an online service that also would supply the bounds of a CRS. I tested downloading the proj4 definitions from epsg.io and it works great, so reprojecting for example geopackage vector data works nice. But I found no way to get the bounds, so that I could calculate the right settings for a generic CRS map view.

berkayoruc commented 4 years ago

On a sidenote: when removing the crs option it works.

That's because WMSTileLayerOptions using Epsg3857 by default see: this.crs = const Epsg3857(), and that just works fine.

The main problem is the provided resolutions in your code. You can create a little helper function which will resolve the resolutions like this:

List<double> getResolutions(double maxX, double minX, int zoom,
    [double tileSize = 256.0]) {
  var size = (maxX - minX) / tileSize;

  return List.generate(zoom, (z) => size / math.pow(2, z));
}

Usually you can get the maxx / minx from the Capabilities http://geoservices.retecivica.bz.it/geoserver/gwc/service/wmts?request=GetCapabilities Then you should find something like this:

      <Layer queryable="1">
        <Name>iier:EK_FA_BOKORCS</Name>
        <Title>EK_FA_BOKORCS</Title>
        <Abstract/>
        <SRS>EPSG:23700</SRS>
        <BoundingBox SRS="EPSG:23700" minx="420000.0" miny="40000.0" maxx="944288.0" maxy="564288.0"/>
      </Layer>

However I couldn't find it. You can get extent from https://epsg.io/ also see https://epsg.io/3857.

Projected bounds:
-20026376.39 -20048966.10
20026376.39 20048966.10

Warning above bounds misleading see the first comment:

The Projected bounds should be wrong.
X domain must be (6378137 * Pi) = 20037508.34

So the right bounds would be:

Projected bounds:
-20037508.342789244 -20037508.342789244
20037508.342789244 20037508.342789244

Here is the final code (I rewrote wms_tile_layer.dart in flutter_map):

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;

import '../widgets/drawer.dart';

List<double> getResolutions(double maxX, double minX, int zoom,
    [double tileSize = 256.0]) {
  var size = (maxX - minX) / tileSize;

  return List.generate(zoom, (z) => size / math.pow(2, z));
}

int r = 6378137;
double _boundsD = r * math.pi;
final Bounds<double> _bounds = Bounds<double>(
  CustomPoint<double>(-_boundsD, -_boundsD),
  CustomPoint<double>(_boundsD, _boundsD),
);

final resolutions = getResolutions(_boundsD, -_boundsD, 19);
final maxZoom = (resolutions.length - 1).toDouble();

final epsg3857CRS = Proj4Crs.fromFactory(
  code: 'EPSG:3857',
  proj4Projection: proj4.Projection('EPSG:3857'),
  resolutions: resolutions,
  bounds: _bounds,
);

class WMSLayerPage extends StatelessWidget {
  static const String route = 'WMS layer';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('WMS Layer')),
      drawer: buildDrawer(context, route),
      body: Padding(
        padding: EdgeInsets.all(8.0),
        child: Column(
          children: [
            Padding(
              padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
              child:
                  Text('This is a map that is showing (46.762784, 11.450838)'),
            ),
            Flexible(
              child: FlutterMap(
                options: MapOptions(
                  center: LatLng(46.762784, 11.450838),
                  zoom: 17,
                  maxZoom: maxZoom,
                  crs: epsg3857CRS,
                  onTap: print,
                ),
                layers: [
                  TileLayerOptions(
                    wmsOptions: WMSTileLayerOptions(
                      crs: epsg3857CRS,
                      baseUrl:
                          'http://geoservices.retecivica.bz.it/geoserver/ows?',
                      layers: ['P_BZ_BASEMAP_HYBRID_EPSG3857'],
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

For example, which are EPSG:5253's minx and maxx? epsg.io 5253 I tried some possibilities, but I took 'Couldn't download or retrieve file.' error.

moovida commented 4 years ago

@berkayoruc that is what I was asking in the comment. You can get the definition as https://epsg.io/5253.proj4, but not the bounds. I am not sure a service is out there that gives that. This makes it almost impossible to do this automatically and therefore make a switchable base for a custom crs flutter_map for example.

berkayoruc commented 4 years ago

@moovida so creating gis data collection app with flutter_map is not good. BTW I need compile input or qfield😄

moovida commented 4 years ago

@moovida so creating gis data collection app with flutter_map is not good.

Let's say I didn't find a smooth way to do that. Maybe it is just that I do not know how.

BTW I need compile input or qfield😄

How do you mix those with flutter? Not really an issue for this repo, but I am curious :-D

berkayoruc commented 4 years ago

@moovida No, I mean I didn't integrate qfield or input in flutter. Maybe C++ files about qgis will can add to flutter project. But this way, I think, will be so diffucult.

maRci002 commented 4 years ago

I'm going to close this issue because main problem has been solved and there isn't any generic solution for determine resolutions / scales even for Leaflet / OpenLayers.