text: # -*- coding: utf-8 -*-
  # -------------------------------------------------------------------------------------------------
  # -- IdentifyFeatures.py
  #    Encapusaltes IdentifyFeatures REST-API from API3 geo.admin.ch
  # -- Author: flu, 09.11.2020
  # -- commandline:
  # -------------------------------------------------------------------------------------------------
  # -- History
  # -- 0.2: flu, 20.11.2020 some issue with json, gets also info if no parz found
  # -------------------------------------------------------------------------------------------------
  
  import json
  import os
  
  # import re
  import sys
  
  import requests
  import requests.packages.urllib3.exceptions as r_ex
  
  
  class IdentifyFeatures:
      """
      Shows how to get a json from https://api3.geo.admin.ch/rest/services/api/MapServer/identify
      Needs a point coordinate and a distance. The distance ist needed to caluculate a square with
      a side length twice the distance an the point in the center. This square is used as a
      intersecting envelope geometry with no buffer
      Needs a IdentifyFeatures.json. In this file there is a dictionary with 3 [T/I/P]config.
      Each config has the query rest endpoint to the tlm_hoheitsgebiet, tlm_bezirksgebiet,
      tlm_kantonsgebiet. These endpoints belong to ESRI Featureservices based on
      https://shop.swisstopo.admin.ch/en/products/landscape/boundaries3D. This data are needed because
      there is no information in geo.admin.ch about the relation between Gemeinde belongs to Bezirk
      belongs to Kanton.
      Each config has the query rest endpoint to the url_ubm_AdminUnit_1 to url_ubm_AdminUnit_6.
      These endpoints belong to ESRI Featureservices based on
      https://eurogeographics.org/maps-for-europe/ebm/ (June 2021 is this dataset not free for use)
      These data are needed to get information about points situated outside of Switzerland. This
      dataset has ETRS89 as source reference system. We assume ETRS89 as equal to WGS84 for
      switzerland and the border are. To transform the coordinates from CHLV95 to ETRS89 we use
      the reframe service from swisstopo https://geodesy.geo.admin.ch/reframe/lv95towgs84
  
      Attributes:
          point:    Where to identify, coordinate pair. First value ist east second is
                    north in LV95 (EPSG code 2056). Default is (2600000.000,1200000.000)
          distance: half length of the square in meter. Default = 0.0
          layers:   comma separated list of technical layer names like
                    ch.swisstopo.pixelkarte-pk25.metadata
      """
  
      def __init__(
          self,
          pt=(2600000.000, 1200000.000),
          distance=0.0,
          layers="ch.swisstopo.pixelkarte-pk25.metadata",
      ):
          """ Inits IdentifyFeatures
  
          sets the attributes
          """
          _cfg = self.__get_settings()
          self.__pt = pt
          self.__distance = distance
          self.__layers = layers
          try:
              self.__envelope = (
                  str(pt[0] - distance)
                  + ","
                  + str(pt[1] - distance)
                  + ","
                  + str(pt[0] + distance)
                  + ","
                  + str(pt[1] + distance)
              )
          except Exception as e:
              raise e
          self.__url_api3_identify = _cfg["api3_identify"]
          self.__url_api3_find = _cfg["api3_find"]
          self.__url_api3_search_server = _cfg["api3_search"]
          self.__url_lv95towgs84 = _cfg["lv95towgs84"]
          self.__url_tlm_hoheitsgebiet = _cfg["T"]["tlm_hoheitsgebiet"]
          self.__url_tlm_bezirksgebiet = _cfg["T"]["tlm_bezirksgebiet"]
          self.__url_tlm_kantonsgebiet = _cfg["T"]["tlm_kantonsgebiet"]
          self.__url_ebm_adminunit_1 = _cfg["I"]["ebm_adminunit_1"]
          self.__url_ebm_adminunit_2 = _cfg["I"]["ebm_adminunit_2"]
          self.__url_ebm_adminunit_3 = _cfg["I"]["ebm_adminunit_3"]
          self.__url_ebm_adminunit_4 = _cfg["I"]["ebm_adminunit_4"]
          self.__url_ebm_adminunit_5 = _cfg["I"]["ebm_adminunit_5"]
          self.__url_ebm_adminunit_6 = _cfg["I"]["ebm_adminunit_6"]
          self.__params = {
              "geometryType": "esriGeometryEnvelope",
              "geometry": self.__envelope,
              "imageDisplay": "0,0,0",
              "mapExtent": "0,0,0,0",
              "tolerance": "0",
              "layers": "all:" + layers,
              "returnGeometry": "false",
              "sr": 2056,
          }
          _status, _results = self.api3identify()
          if len(_results["results"]) > 0:
              self.__lk25 = _results["results"][0]["featureId"]
          else:
              self.__lk25 = ""
  
      @property
      def getpt(self):
          return self.__pt
  
      @property
      def getpt_east(self):
          return self.__pt[0]
  
      @property
      def getpt_north(self):
          return self.__pt[1]
  
      @property
      def getdistance(self):
          return self.__distance
  
      @property
      def getlayers(self):
          return self.__layers
  
      @property
      def getlayerlist(self):
          return list(self.__layers.split(","))
  
      @property
      def getenvelope(self):
          return self.__envelope
  
      @property
      def getlk25(self):
          return self.__lk25
  
      @property
      def __getproxy(self):
          # _proxy = "proxy.admin.ch:8080"
          _proxy = "proxy-bvcol.admin.ch:8080"
          # _proxy = "lf00002a.adb.intra.admin.ch"
          return {"http": _proxy, "https": _proxy}
  
      def api3identify(self):
          """ gets the json from api3.geo.admin.ch identify
          Args:
              -
          Returns:
             json from api3.geo.admin.ch identify
          Raises:
              JSONDecodeError: If the data being deserialized is not a valid JSON document.
          """
          # _proxy = "proxy.admin.ch:8080"
          # _proxy_dict = {"http": _proxy, "https": _proxy}
          # _req = requests.get(
          #     url=self.__url_api3_identify,
          #     proxies=self.__getproxy,
          #     params=self.__params,
          #     verify=False,
          # )
          # return _req.status_code, json.loads(_req.content)
          _status, _json = self.__get_jsonfromurl_proxy(
              self.__url_api3_identify, self.__params
          )
          return _status, json.loads(_json)
  
      def getparzinfo(self, distance=None):
          """ Gets infos to the parzelle in a array of dictionary (ParzNummer,egris_egrid,Gemeinde,
          BFSNummer,Bezirk,Kanton)
          Args:
              optional search distance, overwrites the default distance
          Returns:
              array of dictionary
          Raises:
              -
          """
          _parzinfos = []
          _envelope = ""
          if distance is None:
              _envelope = self.__envelope
          else:
              _envelope = (
                  str(self.getpt_east - distance)
                  + ","
                  + str(self.getpt_north - distance)
                  + ","
                  + str(self.getpt_east + distance)
                  + ","
                  + str(self.getpt_north + distance)
              )
          # serch parcels
          _status_parz, _parzarr = self.__identify(
              _envelope, "ch.kantone.cadastralwebmap-farbe"
          )
          for _parz in _parzarr["results"]:
              _egris_egrid = _parz["attributes"]["egris_egrid"]
              _parcelnumber = _parz["attributes"]["number"]
              _kt = _parz["attributes"]["label"]
              _ok, _gdename = self.__search_gde(_egris_egrid, _kt)
              if _ok:
                  _parzdict = {}
                  _parzdict.update({"egris_egrid": _egris_egrid})
                  _parzdict.update({"ParzNummer": _parcelnumber})
                  _parzdict.update({"Kanton": _kt})
                  _parzdict.update({"Gemeinde": _gdename})
                  _parzdict.update({"BFSNummer": ""})
                  _parzinfos.append(_parzdict)
          # search municipality
          _stauts_gde, _gdearr = self.__identify(
              _envelope, "ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill"
          )
          for _gde in _gdearr["results"]:
              _gdename = _gde["attributes"]["gemname"]
              _kt = _gde["attributes"]["kanton"]
              _bfsnr = _gde["id"]
              _found = False
              # add municipality if no parcel found
              for _idx in range(len(_parzinfos)):
                  if _parzinfos[_idx]["Kanton"] == _kt and (
                      _parzinfos[_idx]["Gemeinde"] == _gdename
                      or _parzinfos[_idx]["Gemeinde"] == ""
                  ):
                      _parzinfos[_idx].update({"BFSNummer": _bfsnr})
                      _found = True
              if not _found:
                  _parzdict = {}
                  _parzdict.update({"egris_egrid": ""})
                  _parzdict.update({"ParzNummer": ""})
                  _parzdict.update({"Kanton": _kt})
                  _parzdict.update({"Gemeinde": _gdename})
                  _parzdict.update({"BFSNummer": _bfsnr})
                  _parzinfos.append(_parzdict)
              if len(_parzinfos) == 0:
                  _parzdict = {}
                  _parzdict.update({"egris_egrid": ""})
                  _parzdict.update({"ParzNummer": ""})
                  _parzdict.update({"Kanton": _kt})
                  _parzdict.update({"Gemeinde": _gdename})
                  _parzdict.update({"BFSNummer": _bfsnr})
                  _parzinfos.append(_parzdict)
          # append bezirk and country
          for _idx in range(len(_parzinfos)):
              if _parzinfos[_idx]["BFSNummer"] == "":
                  _parzinfos[_idx]["BFSNummer"] = self.__find_gde_bfsnr(
                      _parzinfos[_idx]["Gemeinde"], _parzinfos[_idx]["Kanton"]
                  )[1]
              _beznr, _ktnr, _icc = self.__query_hoheitsgebiet(
                  _parzinfos[_idx]["BFSNummer"]
              )
              if _beznr is None:
                  _parzinfos[_idx].update({"Bezirk": ""})
              else:
                  _parzinfos[_idx].update({"Bezirk": self.__query_bezirk_name(_beznr)})
              _parzinfos[_idx].update({"Land": _icc})
              _parzinfos[_idx].update({"LK25NR": self.__lk25})
          _ausland = False
          if len(_parzinfos) == 0:
              _ausland = True
          _ebms = []
          _counter = 0
          while len(_ebms) == 0 and _ausland and _counter < 10:
              _ebms = self.getebm(_counter * 1.0)
              _counter += 1
          if len(_ebms) > 0:
              for _ebm in _ebms:
                  _parzinfos.append(_ebm)
          return json.dumps(_parzinfos)
  
      def getebm(self, dist=0.0):
          _resultarr = []
          _params = {}
          _params.update({"easting": self.__pt[0]})
          _params.update({"northing": self.__pt[1]})
          _params.update({"format": "json"})
          _status, _json = self.__get_jsonfromurl_proxy(self.__url_lv95towgs84, _params)
          _json = json.loads(_json)
          _etrs89_east = _json["easting"]
          _etrs89_north = _json["northing"]
          # get Country
          _params.clear()
          _params.update({"geometry": "{0},{1}".format(_etrs89_east, _etrs89_north)})
          _params.update({"geometryType": "esriGeometryPoint"})
          if dist == 0.0:
              if self.getdistance == 0.0:
                  _params.update({"distance": 0.001})
              else:
                  _params.update({"distance": self.getdistance})
          else:
              _params.update({"distance": dist})
          _params.update({"units": "esriSRUnit_Meter"})
          _params.update({"outFields": "*"})
          _params.update({"returnGeometry": "false"})
          # _params.update({"distance": 1})
          # _params.update({"units": "esriSRUnit_Meter"})
          _params.update({"f": "json"})
          # municipalities DE
          _json = self.__get_jsonfromurl(self.__url_ebm_adminunit_6, _params)["features"]
          for _gdede in _json:
              if _gdede["attributes"]["ICC"] in ["DE"]:
                  _resultarr.append(self.__init_ebmgde(_gdede["attributes"]))
  
          # municipalities FR
          _json = self.__get_jsonfromurl(self.__url_ebm_adminunit_5, _params)["features"]
          for _gdede in _json:
              if _gdede["attributes"]["ICC"] in ["FR"]:
                  _resultarr.append(self.__init_ebmgde(_gdede["attributes"]))
  
          # municipalities not DE | FR | CH | LI
          _json = self.__get_jsonfromurl(self.__url_ebm_adminunit_4, _params)["features"]
          for _gdede in _json:
              if _gdede["attributes"]["ICC"] not in ["DE", "FR", "CH", "LI"]:
                  _resultarr.append(self.__init_ebmgde(_gdede["attributes"]))
  
          # bezirk
          _json = self.__get_jsonfromurl(self.__url_ebm_adminunit_3, _params)["features"]
          for _bezirk in _json:
              if _bezirk["attributes"]["ICC"] not in ["CH", "LI"]:
                  _shnbezirk = _bezirk["attributes"]["SHN"].rstrip("0")
                  for _idx in range(0, len(_resultarr)):
                      if _resultarr[_idx]["SHN"].startswith(_shnbezirk):
                          _resultarr[_idx]["Bezirk"] = _bezirk["attributes"]["NAMN"]
  
          # kanton
          _json = self.__get_jsonfromurl(self.__url_ebm_adminunit_2, _params)["features"]
          for _kanton in _json:
              if _kanton["attributes"]["ICC"] not in ["CH", "LI"]:
                  _shnkanton = _kanton["attributes"]["SHN"].rstrip("0")
                  for _idx in range(0, len(_resultarr)):
                      if _resultarr[_idx]["SHN"].startswith(_shnkanton):
                          _resultarr[_idx]["Kanton"] = _kanton["attributes"]["NAMN"]
  
          # set BFSNummer per country
          for _idx in range(0, len(_resultarr)):
              if _resultarr[_idx]["Land"] == "IT":
                  _resultarr[_idx]["BFSNummer"] = 9996
              elif _resultarr[_idx]["Land"] == "FR":
                  _resultarr[_idx]["BFSNummer"] = 9997
              elif _resultarr[_idx]["Land"] == "DE":
                  _resultarr[_idx]["BFSNummer"] = 9998
              elif _resultarr[_idx]["Land"] == "AT":
                  _resultarr[_idx]["BFSNummer"] = 9999
          return _resultarr
  
      def __init_ebmgde(self, jsondata):
          """ returns dictionary with "gemeinde"
          Args:
              jsondata: attribut tag from query service
          Returns:
              dictionary
          Raises:
          """
          _parzdict = {}
          _parzdict.update({"egris_egrid": ""})
          _parzdict.update({"ParzNummer": ""})
          _parzdict.update({"Kanton": ""})
          _parzdict.update({"Gemeinde": jsondata["NAMN"]})
          _parzdict.update({"BFSNummer": ""})
          _parzdict.update({"Bezirk": ""})
          _parzdict.update({"Land": jsondata["ICC"]})
          _parzdict.update({"SHN": jsondata["SHN"]})
          _parzdict.update({"LK25NR": self.__lk25})
          return _parzdict
  
      def __get_settings(self):
          """ Reads the json file with configuration's for T|I and P
          Args:
              -
          Returns:
              dictionary with the settings
          Raises:
              JSONDecodeError: If the data being deserialized is not a valid JSON document.
              IOError: if the config json file file is missing
          """
          _setting_filename = os.path.join(
              os.path.dirname(sys.argv[0]), "IdentifyFeatures.json"
          )
          try:
              with open(_setting_filename, "r") as _myfile:
                  _data = _myfile.read()
          except IOError:
              raise
  
          # parse file
          try:
              return json.loads(_data)
          except json.JSONDecodeError:
              raise
          except Exception:
              raise
  
      def __get_jsonfromurl(self, url, parameter):
          """ Gets a json from a ArcGISServerURL via a get with parameterlist
          Args:
              url: the url to call
              paramter: paramterd ictionary
          Returns:
              Json
          Raises:
              ConnectionError if there is no connction to the rest end point
          """
          requests.packages.urllib3.disable_warnings(r_ex.InsecureRequestWarning)
          session = requests.Session()
          session.trust_env = False
          try:
              req = session.get(
                  url=url,
                  params=parameter,
                  auth=requests.auth.HTTPDigestAuth(os.environ["USERNAME"], False),
                  verify=False,
              )
          except ConnectionError:
              raise ConnectionError(
                  "Could not get connection to {0} with parameterset {1}".format(
                      url, str(parameter)
                  )
              )
          except Exception:
              raise
          return req.json()
  
      def __get_jsonfromurl_proxy(self, url, parameter):
          """ Gets a json from a serverurl via a get with parameterlist and via proxy
          Args:
              url: the url to call
              paramter: paramterd ictionary
          Returns:
              Json
          Raises:
              ConnectionError if there is no connction to the rest end point
          """
          requests.packages.urllib3.disable_warnings(r_ex.InsecureRequestWarning)
          session = requests.Session()
          session.trust_env = False
          try:
              req = session.get(
                  url=url, proxies=self.__getproxy, params=parameter, verify=False,
              )
          except ConnectionError:
              raise ConnectionError(
                  "Could not get connection to {0} with parameterset {1}".format(
                      url, str(parameter)
                  )
              )
          except Exception:
              raise
          return req.status_code, req.content
  
      def __query_bezirk_name(self, bezirkid=0):
          """ Gets the name of the bezirk from bezirksgebiet if there is no bezirk, enpty string
          Args:
              bezirksnummer
          Returns:
              Bezirksname
          Raises:
              -
          """
          if bezirkid is not None:
              _params = {}
              _params.update(
                  {"where": "BEZIRKSNUMMER={0} and BEZIRK_TEIL < 2".format(bezirkid)}
              )
              _params.update({"outFields": "NAME,BEZIRK_TEIL"})
              _params.update({"returnGeometry": "false"})
              _params.update({"f": "json"})
              _req = self.__get_jsonfromurl(self.__url_tlm_bezirksgebiet, _params)
              _features = _req["features"]
              if len(_features) == 0:
                  return ""
              elif len(_features) == 1:
                  return _features[0]["attributes"]["NAME"]
              else:
                  raise ValueError("To many districts found")
          else:
              return ""
  
      def __query_hoheitsgebiet(self, bfsnummer=0):
          """ Gets the bezirksnummer, kantonsnummer and ICC county code from the hoheitsgebiet
          Args:
              gemeinde bfsnummer
          Returns:
              bezirksnummer, kantonsnummer, ICC county code
          Raises:
              -
          """
          _params = {}
          _params.update({"where": "BFS_NUMMER={0} and GEM_TEIL < 2".format(bfsnummer)})
          _params.update({"outFields": "GEM_TEIL,BEZIRKSNUMMER,KANTONSNUMMER,ICC"})
          _params.update({"returnGeometry": "false"})
          _params.update({"f": "json"})
          _req = self.__get_jsonfromurl(self.__url_tlm_hoheitsgebiet, _params)
          _features = _req["features"]
          if len(_features) == 1:
              return (
                  _features[0]["attributes"]["BEZIRKSNUMMER"],
                  _features[0]["attributes"]["KANTONSNUMMER"],
                  _features[0]["attributes"]["ICC"],
              )
          else:
              raise ValueError("To many municipalities found")
  
      def __find_gde_bfsnr(self, gde="", kt=""):
          """ Gets the gemeinde bfsnummer from ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill
          Args:
              gemeinde name, kantonskuerzel
          Returns:
              true if found, false if not found and gemeinde bfsnummer
          Raises:
              -
          """
          _params = {}
          _params.update(
              {"layer": "ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill"}
          )
          _params.update({"searchText": "{0}".format(gde)})
          _params.update({"searchField": "gemname"})
          _params.update({"returnGeometry": "false"})
          _req = requests.get(
              url=self.__url_api3_find,
              proxies=self.__getproxy,
              params=_params,
              verify=False,
          )
          if _req.status_code == 200:
              _json = json.loads(_req.content)
              _results = _json["results"]
              # print(_results)
              if len(_results) == 0:
                  return False, "gemname not found"
              else:
                  for _ele in _results:
                      if _ele["attributes"]["kanton"] == kt:
                          return True, _ele["id"]
                  return False, "gemname in kanton not found"
          else:
              return False, _req.content
  
      def __search_gde(self, location, kt):
          """ Gets the Gemeindename
          Args:
              egris_egrid
          Returns:
              True if found, false if not found and gemeindename
          Raises:
              -
          """
          _params = {}
          _params.update({"searchText": "{0}".format(location)})
          _params.update({"type": "locations"})
          _req = requests.get(
              url=self.__url_api3_search_server,
              proxies=self.__getproxy,
              params=_params,
              verify=False,
          )
          if _req.status_code == 200:
              _json = json.loads(_req.content)
              _results = _json["results"]
              if len(_results) == 1:
                  # _clean = re.compile("<.*?>")
                  # return (
                  #    True,
                  #    re.sub(_clean, "", _results[0]["attrs"]["label"].split(" ")[0]),
                  # )
                  _label = _results[0]["attrs"]["label"]
                  return True, _label[3 : _label.find("</b>")]
                  # _sa = _results[0]["attrs"]["label"].split(" ")
                  # if len(_sa) == 6:
                  #     return True, re.sub(_clean, "", _sa[0])
                  # elif len(_sa) == 7:
                  #     return True, re.sub(_clean, "", "{0} {1}".format(_sa[0], _sa[1]))
                  # elif len(_sa) == 8:
                  #     return (
                  #         True,
                  #         re.sub(
                  #             _clean, "", "{0} {1} {2}".format(_sa[0], _sa[1], _sa[2])
                  #         ),
                  #     )
                  # elif len(_sa) == 9:
                  #     return (
                  #         True,
                  #         re.sub(
                  #             _clean,
                  #             "",
                  #             "{0} {1} {2} {3}".format(_sa[0], _sa[1], _sa[2], _sa[3]),
                  #         ),
                  #     )
                  #
                  # else:
                  #     False, "could not decode label"
              else:
                  return False, "egris_egrid not found"
          else:
              return False, _req.content
  
      def __identify(self, envelope, layer):
          """ calls api3 identify with one layer
          Args:
              envelope, search box
              layer, technical layername from map.geo.admin.ch
          Returns:
              statuscode, 200 if ok
              json result
          Raises:
              -
          """
          _parms1 = self.__params
          _parms1["geometry"] = envelope
          _parms1["layers"] = "all:{0}".format(layer)
  
          _req = requests.get(
              url=self.__url_api3_identify,
              proxies=self.__getproxy,
              params=_parms1,
              verify=False,
          )
          return _req.status_code, json.loads(_req.content)
  