├── ClipToHemisphere.py ├── README.md ├── __init__.py ├── metadata.txt └── orthographic_lat_20_lon_30.png /ClipToHemisphere.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | __init__.py 6 | --------------------- 7 | Date : September 2020 8 | Copyright : (C) 2020 by Juernjakob Dugge 9 | Email : juernjakob at gmail dot com 10 | *************************************************************************** 11 | * * 12 | * This program is free software; you can redistribute it and/or modify * 13 | * it under the terms of the GNU General Public License as published by * 14 | * the Free Software Foundation; either version 2 of the License, or * 15 | * (at your option) any later version. * 16 | * * 17 | *************************************************************************** 18 | """ 19 | 20 | __author__ = 'Juernjakob Dugge' 21 | __date__ = 'September 2020' 22 | __copyright__ = '(C) 2020, Juernjakob Dugge' 23 | 24 | import os 25 | 26 | from qgis.core import ( 27 | QgsApplication, 28 | QgsProcessing, 29 | QgsProcessingParameterFeatureSource, 30 | QgsProcessingParameterNumber, 31 | QgsProcessingProvider, 32 | QgsCoordinateReferenceSystem, 33 | QgsVectorLayer, 34 | QgsFeature, 35 | QgsGeometry, 36 | QgsCoordinateTransform, 37 | QgsPointXY, 38 | QgsProject, 39 | QgsProcessingParameterVectorDestination 40 | ) 41 | 42 | from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm 43 | import processing 44 | import numpy as np 45 | import cmath 46 | 47 | pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] 48 | 49 | 50 | class ClipToHemisphereProvider(QgsProcessingProvider): 51 | 52 | def __init__(self): 53 | QgsProcessingProvider.__init__(self) 54 | 55 | def unload(self): 56 | pass 57 | 58 | def loadAlgorithms(self): 59 | self.addAlgorithm(ClipToHemisphereAlgorithm()) 60 | 61 | def id(self): 62 | """ 63 | Returns the unique provider id, used for identifying the provider. This 64 | string should be a unique, short, character only string, eg "qgis" or 65 | "gdal". This string should not be localised. 66 | """ 67 | return 'Clip to hemisphere' 68 | 69 | def name(self): 70 | """ 71 | Returns the provider name, which is used to describe the provider 72 | within the GUI. 73 | 74 | This string should be short (e.g. "Lastools") and localised. 75 | """ 76 | return self.tr('Clip to hemisphere') 77 | 78 | def longName(self): 79 | """ 80 | Returns the a longer version of the provider name, which can include 81 | extra details such as version numbers. E.g. "Lastools LIDAR tools 82 | (version 2.2.1)". This string should be localised. The default 83 | implementation returns the same string as name(). 84 | """ 85 | return self.name() 86 | 87 | 88 | class ClipToHemispherePlugin(object): 89 | def __init__(self): 90 | self.provider = None 91 | 92 | def initProcessing(self): 93 | self.provider = ClipToHemisphereProvider() 94 | QgsApplication.processingRegistry().addProvider(self.provider) 95 | 96 | def initGui(self): 97 | self.initProcessing() 98 | 99 | def unload(self): 100 | QgsApplication.processingRegistry().removeProvider(self.provider) 101 | 102 | 103 | class ClipToHemisphereAlgorithm(QgisAlgorithm): 104 | INPUT = 'INPUT' 105 | OUTPUT = 'OUTPUT' 106 | CENTER_LATITUDE = 'CENTER_LATITUDE' 107 | CENTER_LONGITUDE = 'CENTER_LONGITUDE' 108 | SEGMENTS = 'SEGMENTS' 109 | 110 | def __init__(self): 111 | super().__init__() 112 | 113 | def initAlgorithm(self, config=None): 114 | self.addParameter( 115 | QgsProcessingParameterFeatureSource( 116 | self.INPUT, 117 | self.tr('Input layer'), 118 | [QgsProcessing.TypeVectorAnyGeometry] 119 | ) 120 | ) 121 | 122 | self.addParameter( 123 | QgsProcessingParameterVectorDestination( 124 | self.OUTPUT, 125 | self.tr('Output layer') 126 | ) 127 | ) 128 | 129 | param = QgsProcessingParameterNumber( 130 | self.CENTER_LATITUDE, 131 | self.tr('Center latitude'), 132 | type=QgsProcessingParameterNumber.Double, 133 | minValue=-90, 134 | maxValue=90 135 | ) 136 | param.setMetadata({'widget_wrapper': 137 | {'decimals': 1} 138 | }) 139 | self.addParameter(param) 140 | 141 | param = QgsProcessingParameterNumber( 142 | self.CENTER_LONGITUDE, 143 | self.tr('Center longitude'), 144 | type=QgsProcessingParameterNumber.Double, 145 | minValue=-180, 146 | maxValue=180 147 | ) 148 | param.setMetadata({'widget_wrapper': 149 | {'decimals': 1} 150 | }) 151 | self.addParameter(param) 152 | 153 | self.addParameter(QgsProcessingParameterNumber( 154 | self.SEGMENTS, 155 | self.tr('Segments'), 156 | defaultValue=720, 157 | minValue=3, 158 | type=QgsProcessingParameterNumber.Integer 159 | )) 160 | 161 | def name(self): 162 | return 'cliptohemisphere' 163 | 164 | def displayName(self): 165 | return self.tr('Clip to hemisphere') 166 | 167 | def processAlgorithm(self, parameters, context, feedback): 168 | source = self.parameterAsSource(parameters, self.INPUT, context) 169 | sourceCrs = source.sourceCrs() 170 | centerLatitude = self.parameterAsDouble(parameters, 171 | self.CENTER_LATITUDE, context) 172 | centerLongitude = self.parameterAsDouble(parameters, 173 | self.CENTER_LONGITUDE, context) 174 | segments = self.parameterAsInt(parameters, self.SEGMENTS, context) 175 | 176 | earthRadius = 6371000 177 | targetProjString = "+proj=ortho +lat_0=" + str(centerLatitude) + \ 178 | " +lon_0=" + str(centerLongitude) + \ 179 | " +x_0=0 +y_0=0 +a=" + str(earthRadius) + \ 180 | " +b=" + str(earthRadius) + \ 181 | " +units=m +no_defs" 182 | targetCrs = QgsCoordinateReferenceSystem() 183 | targetCrs.createFromProj(targetProjString) 184 | 185 | transformTargetToSrc = QgsCoordinateTransform(targetCrs, 186 | sourceCrs, 187 | QgsProject.instance()).transform 188 | transformSrcToTarget = QgsCoordinateTransform(sourceCrs, 189 | targetCrs, 190 | QgsProject.instance()).transform 191 | clipLayer = QgsVectorLayer("MultiPolygon", "clipLayer", "memory") 192 | pr = clipLayer.dataProvider() 193 | 194 | # Handle edge cases: 195 | # Hemisphere centered on the equator 196 | if centerLatitude == 0: 197 | # Hemisphere centered on the equator and including the antimeridian 198 | if abs(centerLongitude) >= 90: 199 | edgeEast = -180 - np.sign(centerLongitude) * \ 200 | (180 - abs(centerLongitude)) + 90 201 | edgeWest = 180 - np.sign(centerLongitude) * \ 202 | (180 - abs(centerLongitude)) - 90 203 | circlePoints = [[ 204 | [QgsPointXY(-180.01, latitude) for 205 | latitude in np.linspace(90, -90, segments // 8)] + 206 | [QgsPointXY(longitude, -90) for longitude in 207 | np.linspace(-180, edgeEast, segments // 8)] + 208 | [QgsPointXY(edgeEast, latitude) for latitude in 209 | np.linspace(-90, 90, segments // 8)] + 210 | [QgsPointXY(longitude, 90) for longitude in 211 | np.linspace(edgeEast, -180, segments // 8)] 212 | ], 213 | [ 214 | [QgsPointXY(edgeWest, latitude) for latitude in 215 | np.linspace(90, -90, segments // 8)] + 216 | [QgsPointXY(longitude, -90) for longitude in 217 | np.linspace(edgeWest, 180, segments // 8)] + 218 | [QgsPointXY(180.01, latitude) for 219 | latitude in np.linspace(-90, 90, segments // 8)] + 220 | [QgsPointXY(longitude, 90) for longitude in 221 | np.linspace(180, edgeWest, segments // 8)] 222 | ]] 223 | # Hemisphere centered on the equator not including the antimeridian 224 | else: 225 | edgeWest = centerLongitude - 90 226 | edgeEast = centerLongitude + 90 227 | circlePoints = [[ 228 | [QgsPointXY(edgeWest, latitude) for latitude in 229 | np.linspace(90, -90, segments // 4)] + 230 | [QgsPointXY(longitude, -90) for longitude in 231 | np.linspace(edgeWest, edgeEast, segments // 4)] + 232 | [QgsPointXY(edgeEast, latitude) for 233 | latitude in np.linspace(-90, 90, segments // 4)] + 234 | [QgsPointXY(longitude, 90) for longitude in 235 | np.linspace(edgeEast, edgeWest, segments // 4)] 236 | ]] 237 | # Hemisphere centered on one of the poles 238 | elif abs(centerLatitude) == 90: 239 | circlePoints = [[ 240 | [QgsPointXY(-180.01, latitude) for latitude in 241 | np.linspace(45 + 0.5 * centerLatitude, 242 | -45 + 0.5 * centerLatitude, 243 | segments // 4)] + 244 | [QgsPointXY(longitude, -45 + 0.5 * centerLatitude) 245 | for longitude in 246 | np.linspace(-180, 180, segments // 4)] + 247 | [QgsPointXY(180.01, latitude) for latitude in 248 | np.linspace(-45 + 0.5 * centerLatitude, 249 | 45 + 0.5 * centerLatitude, 250 | segments // 4)] + 251 | [QgsPointXY(longitude, 45 + 0.5 * centerLatitude) for longitude 252 | in 253 | np.linspace(180, -180, segments // 4)] 254 | ]] 255 | # All other hemispheres 256 | else: 257 | # Create a circle in the orthographic projection, convert the 258 | # circle coordinates to the source CRS 259 | angles = np.linspace(0, 2 * np.pi, segments, endpoint=False) 260 | circlePoints = np.array([ 261 | transformTargetToSrc( 262 | QgsPointXY(np.cos(angle) * earthRadius * 0.9999, 263 | np.sin(angle) * earthRadius * 0.9999) 264 | ) for angle in angles 265 | ]) 266 | 267 | # Sort the projected circle coordinates from west to east 268 | sortIdx = np.argsort(circlePoints[:, 0]) 269 | circlePoints = circlePoints[sortIdx, :] 270 | circlePoints = [[[QgsPointXY(point[0], point[1]) 271 | for point in circlePoints]]] 272 | 273 | # Find the circle point in the orthographic projection that lies 274 | # on the antimeridian by linearly interpolating the angles of the 275 | # first and last coordinates 276 | startGap = 180 + circlePoints[0][0][0][0] 277 | endGap = 180 - circlePoints[0][0][-1][0] 278 | totalGap = startGap + endGap 279 | startCoordinates = transformSrcToTarget(circlePoints[0][0][0]) 280 | endCoordinates = transformSrcToTarget(circlePoints[0][0][-1]) 281 | startAngle = np.arctan2(startCoordinates[0], startCoordinates[1]) 282 | endAngle = np.arctan2(endCoordinates[0], endCoordinates[1]) 283 | antimeridianAngle = cmath.phase( 284 | endGap / totalGap * cmath.rect(1, startAngle) + 285 | startGap / totalGap * cmath.rect(1, endAngle)) 286 | antimeridianPoint = transformTargetToSrc(QgsPointXY( 287 | np.sin(antimeridianAngle) * earthRadius * 0.9999, 288 | np.cos(antimeridianAngle) * earthRadius * 0.9999 289 | )) 290 | 291 | # Close the polygon 292 | circlePoints[0][0].extend( 293 | [QgsPointXY(180.01, latitude) for latitude in 294 | np.linspace(antimeridianPoint[1], 295 | np.sign(centerLatitude) * 90, segments // 4)] + 296 | [QgsPointXY(-180.01, latitude) for latitude in 297 | np.linspace(np.sign(centerLatitude) * 90, 298 | antimeridianPoint[1], segments // 4)] 299 | ) 300 | 301 | # Create the feature and add it to the layer 302 | circle = QgsFeature() 303 | circle.setGeometry(QgsGeometry.fromMultiPolygonXY(circlePoints)) 304 | 305 | pr.addFeatures([circle]) 306 | 307 | result = processing.run('native:intersection', { 308 | 'INPUT': parameters['INPUT'], 309 | 'OVERLAY': clipLayer, 310 | 'OUTPUT': parameters['OUTPUT'] 311 | }, is_child_algorithm=True, context=context, feedback=feedback) 312 | 313 | return {self.OUTPUT: result['OUTPUT']} 314 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ClipToHemisphere 2 | ================ 3 | 4 | Plugin for the QGIS Processing framework to clip vector layers to one hemisphere 5 | 6 | When projecting data to the orthographic projection, features that cross the horizon can lead to unsightly artefacts. 7 | By clipping the data to the hemisphere that will be visible in the orthographic projection, these artifacts can be 8 | avoided. This plugin for the QGIS Processing framework takes a vector layer and a latitude and longitude for the 9 | center of the hemisphere and clips the vector layer accordingly. The plugin takes care of special cases such as a hemisphere centered on the equator or on one of the poles. 10 | 11 | ![Example output: Natural Earth data projected to an orthographic projection centered on 20°N 30°E](orthographic_lat_20_lon_30.png) 12 | 13 | Usage 14 | ----- 15 | 16 | 1. Load the vector layer you want to project to an orthographic projection. Note that the vector layer must be in WGS 84, so reproject the layer to the EPSG:4326 projection if necessary. Setting the project CRS to EPSG:4326 with on-the-fly projection is not sufficient, the layer itself has to be in the right projection. 17 | 2. In the Processing Toolbox, run the `Clip to Hemisphere | Clip to Hemisphere | Clip a vector layer to the hemisphere centred on a user specified point` algorithm. 18 | 3. In the Parameters tab of the algorithm, choose the latitude and longitude of the central point of the desired orthographic projection in degrees. The algorithm assumes the central point to be given in WGS 84 coordinates. 19 | 4. Either choose a file name for the clipped output, or leave the field empty to create a temporary memory layer. 20 | 5. Choose run. The clipped layer will be added to the project. 21 | 6. If you haven't done so already, create a custom CRS for the orthographic projection using the `Settings | Custom CRS` menu. The projection string should be similar to `+proj=ortho +lat_0=20 +lon_0=30 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs`, replacing the values for `lat_0` and `lon_0` with the central point of the desired projection. 22 | 7. Disable the `Simplify geometry` feature for the clipped layer in the `Rendering` tab of the Layer properties. 23 | 8. In the `CRS` tab of the Project properties, choose the newly created custom CRS and check the `Enable 'on the fly' reprojection`. 24 | 25 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | *************************************************************************** 5 | __init__.py 6 | --------------------- 7 | Date : September 2020 8 | Copyright : (C) 2020 by Juernjakob Dugge 9 | Email : juernjakob at gmail dot com 10 | *************************************************************************** 11 | * * 12 | * This program is free software; you can redistribute it and/or modify * 13 | * it under the terms of the GNU General Public License as published by * 14 | * the Free Software Foundation; either version 2 of the License, or * 15 | * (at your option) any later version. * 16 | * * 17 | *************************************************************************** 18 | """ 19 | 20 | __author__ = 'Juernjakob Dugge' 21 | __date__ = 'September 2020' 22 | __copyright__ = '(C) 2020, Juernjakob Dugge' 23 | 24 | 25 | def classFactory(iface): 26 | from .ClipToHemisphere import ClipToHemispherePlugin 27 | return ClipToHemispherePlugin() 28 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=Clip to Hemisphere 3 | qgisMinimumVersion=2.99 4 | qgisMaximumVersion=3.99 5 | description=Clip vector layers to the hemisphere of the earth centered on a user 6 | specified point in order to use them in an orthographic projection without 7 | introducing artefacts. 8 | category=Vector 9 | version=0.4 10 | author=Juernjakob Dugge 11 | email=juernjakob@gmail.com 12 | homepage=https://github.com/jdugge/ClipToHemisphere 13 | tracker=https://github.com/jdugge/ClipToHemisphere/issues 14 | repository=https://github.com/jdugge/ClipToHemisphere 15 | experimental=False 16 | about=When projecting data to the orthographic projection, features that cross 17 | the horizon can lead to unsightly artifacts. By clipping the data to the 18 | hemisphere that will be visible in the orthographic projection, these 19 | artifacts can be avoided. This plugin for the QGIS Processing framework 20 | takes a vector layer and a latitude and longitude for the center of the 21 | hemisphere and clips the vector layer accordingly. The plugin takes care of 22 | special cases such as a hemisphere centered on the equator or on one of the 23 | poles. 24 | changelog=0.4 - Made plugin compatible with QGIS 3 25 | 0.3 - Added warning if input layer does not use EPSG:4326 26 | 0.2 - Fixed issue with linear features at 180° W/E, made plugin 27 | compatible with QGIS 2.18 28 | 0.1 - Initial version -------------------------------------------------------------------------------- /orthographic_lat_20_lon_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdugge/ClipToHemisphere/1b8b138422e2aab99a46420abc3916e723e031f0/orthographic_lat_20_lon_30.png --------------------------------------------------------------------------------