# Copyright (C) 2017 Antoine Fourmy <antoine dot fourmy at gmail dot com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import OrderedDict from os.path import join from .base_view import BaseView from math import asin, cos, radians, sin, sqrt try: import shapefile import shapely.geometry from pyproj import Proj except ImportError as e: import warnings warnings.warn(str(e)) warnings.warn('SHP librairies missing: pyNMS will not start') warnings.warn('please install "pyshp", "shapely", and "pyproj" with pip') from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtGui import ( QBrush, QPen, QColor, QDrag, QPainter, QPixmap ) from PyQt5.QtWidgets import ( QFrame, QPushButton, QWidget, QApplication, QLabel, QGraphicsItem, QGraphicsLineItem, QGraphicsPixmapItem, QGroupBox, ) class GeographicalView(BaseView): def __init__(self, controller): super().__init__(controller) # initialize the map self.world_map = Map(self) def update_geographical_coordinates(self, *gnodes): for gnode in gnodes: lon, lat = self.world_map.to_geographical_coordinates(gnode.x, gnode.y) gnode.node.longitude, gnode.node.latitude = lon, lat def update_logical_coordinates(self, *gnodes): for gnode in gnodes: gnode.node.logical_x, gnode.node.logical_y = gnode.x, gnode.y def move_to_geographical_coordinates(self, *gnodes): if not gnodes: gnodes = self.all_gnodes() for gnode in gnodes: gnode.x, gnode.y = self.world_map.to_canvas_coordinates( gnode.node.longitude, gnode.node.latitude ) def move_to_logical_coordinates(self, *gnodes): if not gnodes: gnodes = self.all_gnodes() for gnode in gnodes: gnode.x, gnode.y = gnode.node.logical_x, gnode.node.logical_y def haversine_distance(self, s, d): coord = (s.longitude, s.latitude, d.longitude, d.latitude) # decimal degrees to radians conversion lon_s, lat_s, lon_d, lat_d = map(radians, coord) delta_lon = lon_d - lon_s delta_lat = lat_d - lat_s a = sin(delta_lat/2)**2 + cos(lat_s)*cos(lat_d)*sin(delta_lon/2)**2 c = 2*asin(sqrt(a)) # radius of earth (km) r = 6371 return c*r class Map(): projections = OrderedDict([ ('Spherical', Proj('+proj=ortho +lat_0=48 +lon_0=17')), ('Mercator', Proj(init='epsg:3395')), ('WGS84', Proj(init='epsg:3857')), ('ETRS89 - LAEA Europe', Proj("+init=EPSG:3035")) ]) def __init__(self, view): self.view = view self.proj = 'Spherical' self.ratio, self.offset = 1/1000, (0, 0) self.display = True self.polygons = self.view.scene.createItemGroup([]) # brush for water and lands self.water_brush = QBrush(QColor(64, 164, 223)) self.land_brush = QBrush(QColor(52, 165, 111)) self.land_pen = QPen(QColor(52, 165, 111)) def to_geographical_coordinates(self, x, y): px, py = (x - self.offset[0])/self.ratio, (self.offset[1] - y)/self.ratio return self.projections[self.proj](px, py, inverse=True) def to_canvas_coordinates(self, longitude, latitude): px, py = self.projections[self.proj](longitude, latitude) return px*self.ratio + self.offset[0], -py*self.ratio + self.offset[1] def draw_water(self): if self.proj in ('Spherical', 'ETRS89 - LAEA Europe'): cx, cy = self.to_canvas_coordinates(17, 48) # if the projection is ETRS89, we need the diameter and not the radius R = 6371000*self.ratio*(1 if self.proj == 'Spherical' else 2) earth_water = QtWidgets.QGraphicsEllipseItem(cx - R, cy - R, 2*R, 2*R) earth_water.setZValue(0) earth_water.setBrush(self.water_brush) self.polygons.addToGroup(earth_water) else: # we compute the projected bounds of the Mercator (3395) projection # upper-left corner x and y coordinates: ulc_x, ulc_y = self.to_canvas_coordinates(-180, 84) # lower-right corner x and y coordinates lrc_x, lrc_y = self.to_canvas_coordinates(180, -84.72) # width and height of the map (required for the QRectItem) width, height = lrc_x - ulc_x, lrc_y - ulc_y earth_water = QtWidgets.QGraphicsRectItem(ulc_x, ulc_y, width, height) earth_water.setZValue(0) earth_water.setBrush(self.water_brush) self.polygons.addToGroup(earth_water) def draw_polygons(self): sf = shapefile.Reader(self.shapefile) polygons = sf.shapes() for polygon in polygons: # convert shapefile geometries into shapely geometries # to extract the polygons of a multipolygon polygon = shapely.geometry.shape(polygon) # if it is a polygon, we use a list to make it iterable if polygon.geom_type == 'Polygon': polygon = [polygon] for land in polygon: qt_polygon = QtGui.QPolygonF() longitudes, latitudes = land.exterior.coords.xy for lon, lat in zip(longitudes, latitudes): px, py = self.to_canvas_coordinates(lon, lat) if px > 1e+10: continue qt_polygon.append(QtCore.QPointF(px, py)) polygon_item = QtWidgets.QGraphicsPolygonItem(qt_polygon) polygon_item.setBrush(self.land_brush) polygon_item.setPen(self.land_pen) polygon_item.setZValue(1) yield polygon_item def show_hide_map(self): self.display = not self.display self.polygons.show() if self.display else self.polygons.hide() def delete_map(self): self.view.scene.removeItem(self.polygons) def redraw_map(self): self.delete_map() self.polygons = self.view.scene.createItemGroup(self.draw_polygons()) self.draw_water() # replace the nodes at their geographical location self.view.move_to_geographical_coordinates()