├── LICENSE.md ├── README.md ├── examples ├── all_projections.py ├── example1.py └── optimize_projection.py ├── setup.py └── skymapper ├── __init__.py ├── healpix.py ├── map.py ├── projection.py └── survey ├── __init__.py ├── boss_survey.ply └── des-round17-poly_tidy.ply /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Melchior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/skymapper.svg)](https://pypi.python.org/pypi/skymapper/) 2 | [![License](https://img.shields.io/github/license/pmelchior/skymapper.svg)](https://github.com/pmelchior/skymapper/blob/master/LICENSE.md) 3 | 4 | # Skymapper 5 | 6 | *A collection of matplotlib instructions to map astronomical survey data from the celestial sphere onto 2D.* 7 | 8 | The purpose of this package is to facilitate interactive work as well as the the creation of publication-quality plots with a python-based workflow many astronomers are accustomed to. The primary motivation is a truthful representation of samples and fields from the curved sky in planar figures, which becomes relevant when sizable portions of the sky are observed. 9 | 10 | What can it do? For instance, find the optimal projection for a given list of spherical coordinates and [creating a density map](examples/example1.py) from a catalog in a few lines: 11 | 12 | ```python 13 | import skymapper as skm 14 | 15 | # 1) construct a projection, here Albers 16 | # lon_0, lat_0: longitude/latitude that map onto 0/0 17 | # lat_1, lat_2: reference latitudes for conic projection 18 | lon_0, lat_0, lat_1, lat_2 = 27.35, -37.04, -57.06, -11.34 19 | proj = skm.Albers(lon_0, lat_0, lat_1, lat_2) 20 | 21 | # alternative: define the optimal projection for set of coordinates 22 | # by minimizing the variation in distortion 23 | crit = skm.stdDistortion 24 | proj = skm.Albers.optimize(ra, dec, crit=crit) 25 | 26 | # 2) construct map: will hold figure and projection 27 | # the outline of the sphere can be styled with kwargs for matplotlib Polygon 28 | map = skm.Map(proj) 29 | 30 | # 3) add graticules, separated by 15 deg 31 | # the lines can be styled with kwargs for matplotlib Line2D 32 | # additional arguments for formatting the graticule labels 33 | sep = 15 34 | map.grid(sep=sep) 35 | 36 | # 4) add data to the map, e.g. 37 | # make density plot 38 | nside = 32 39 | mappable = map.density(ra, dec, nside=nside) 40 | cb = map.colorbar(mappable, cb_label="$n_g$ [arcmin$^{-2}$]") 41 | 42 | # add scatter plot 43 | map.scatter(ra_scatter, dec_scatter, s=size_scatter, edgecolor='k', facecolor='None') 44 | 45 | # focus on relevant region 46 | map.focus(ra, dec) 47 | ``` 48 | 49 | ![Random density in DES footprint](https://github.com/pmelchior/skymapper/assets/1463403/fa1528f4-2ed0-4945-a342-17bddd64d73d) 50 | 51 | The `map` instance has several members, most notably 52 | 53 | * `fig`: the `matplotlib.Figure` that holds the map 54 | * `ax`: the `matplotlib.Axes` that holds the map 55 | 56 | The syntax mimics `matplotlib` as closely as possible. Currently supported are canonical plotting functions 57 | 58 | * `plot` 59 | * `scatter` 60 | * `hexbin` for binning and interpolating samples 61 | * `colorbar` with an optional argument `cb_label` to set the label 62 | * `text` with an optional `direction in ['parallel','meridian']` argument to align along either graticule 63 | 64 | as well as special functions 65 | 66 | * `footprint` to show the region covered by a survey 67 | * `vertex` to plot a list of simple convex polygons 68 | * `healpix` to plot a healpix map as a list of polygons 69 | * `density` to create a density map in healpix cells 70 | * `extrapolate` to generate a field from samples over the entire sky or a subregion 71 | 72 | Exploratory and interactive workflows are specifically supported. For instance, you can zoom and pan, also scroll in/out (google-maps style), and the `map` will automatically update the location of the graticule labels, which are not regularly spaced. 73 | 74 | The styling of graticules can be changed by calling `map.grid()` with different parameters. Finer-grained access is provided by 75 | 76 | * `map.labelParallelsAtFrame()` creates/styles the vertical axis labels at the intersection of the grid parallels 77 | * `map.labelMeridiansAtFrame()` creates/styles the horizontal axis labels at the intersection of the grid meridians 78 | * `map.labelParallelsAtMeridian()` creates/styles parallels at a given meridian (useful for all-sky maps) 79 | * `map.labelMeridiansAtParallel()` creates/styles meridians at a given parallel (useful for all-sky maps) 80 | 81 | ## Installation and Prerequisites 82 | 83 | You can either clone the repo and install with `pip install .` or get the latest release with `pip install skymapper`. 84 | 85 | Dependencies: 86 | 87 | * numpy 88 | * scipy 89 | * matplotlib 90 | * healpy 91 | 92 | For survey footprints, you'll need [`pymangle`](https://github.com/esheldon/pymangle). 93 | 94 | ## Background 95 | 96 | The essential parts of the workflow are 97 | 98 | 1. Creating the `Projection`, e.g. `Hammer`, `Albers`, `WagnerIV` 99 | 2. Setting up a `Map` to hold the projection and matplotlib figure, ax, ... 100 | 3. Add data to the map 101 | 102 | Several map projections are available, the full list is stored in the dictionary `projection_register`. If the projection you want isn't included, open an issue, or better: create it yourself (see below) and submit a pull request. 103 | 104 | There are two conventions for longitudes in astronomy. The standard frame, used for instance for world maps or Galactic maps, has a longitudinal coordinates in the range [-180 .. 180] deg, which increase west to east (in other words, on the map east is right). The equatorial (RA/Dec) frame is left-handed (i.e. on the map east is left) and has coordinates in the range [0 .. 360] deg. To determine the convention, `Projection` has an argument `lon_type`, which can be either `"lon"` or `"ra"` for standard or equatorial, respectively. The default is `lon_type="ra"`. 105 | 106 | Map projections can preserve sky area, angles, or distances, but never all three. That means defining a suitable projection must be a compromise. For most applications, sizes should exactly be preserved, which means that angles and distances may not be. The optimal projection for a given list of `ra`, `dec` can be found by calling: 107 | 108 | 109 | ```python 110 | crit = skm.projection.stdDistortion 111 | proj = skm.Albers.optimize(ra, dec, crit=crit) 112 | ``` 113 | 114 | This optimizes the `Albers` projection parameters to minimize the variance of the map distortion (i.e. the apparent ellipticity of a true circle on the sky). Alternative criteria are e.g. `maxDistortion` or `stdScale` (for projections that are not equal-area). 115 | 116 | ### Creating a custom projection 117 | 118 | For constructing your own projection, derive from [`Projection`](skymapper/projection.py). You'll see that every projection needs to implement at least these methods: 119 | 120 | * `transform` to map from spherical to map coordinates x/y 121 | * `invert` to map from x/y to spherical (if not implemented defaults to basic and slow BFGS inversion) 122 | 123 | If the projection has several parameters, you will want to create a special `@classmethod optimize` because the default one only determines the best longitude reference. An example for that is given in e.g. `ConicProjection.optimize`. 124 | 125 | ### Creating/using a survey 126 | 127 | Several surveys are predefined and listed in the `survey_register` dictionary. If the survey you want isn't included, don't despair. To create one can derive a class from [`Survey`](skymapper/survey/__init__.py), which only needs to implement one method: 128 | 129 | ​ `def contains(self, ra, dec)` to determine whether RA, Dec are inside the footprint. 130 | 131 | If this looks like the [`pymangle`](https://github.com/esheldon/pymangle) interface: it should. That means that you can avoid the overhead of having to define a survey and e.g. pass a `pymangle.Mangle` object directly to `footprint()`. 132 | 133 | ### Limitation(s) 134 | 135 | The combination of `Map` and `Projection` is *not* a [matplotlib transformation](http://matplotlib.org/users/transforms_tutorial.html). Among several reasons, it is very difficult (maybe impossible) to work with the `matplotlib.Axes` that are not rectangles or ellipses. So, we decided to split the problem: making use of matplotlib for lower-level graphics primitive and layering the map-making on top of it. This way, we can control e.g. the interpolation method on the sphere or the location of the tick labels in a way consistent with visual expectations from hundreds of years of cartography. While `skymapper` tries to follow matplotlib conventions very closely, some methods may not work as expected. Open an issue if you think you found such a case. 136 | 137 | In particular, we'd appreciate help to make sure that the interactive features work well on all matplotlib backends. 138 | -------------------------------------------------------------------------------- /examples/all_projections.py: -------------------------------------------------------------------------------- 1 | import skymapper as skm 2 | import matplotlib.pyplot as plt 3 | 4 | if __name__ == "__main__": 5 | # cycle through all defined projections and show the full sky 6 | # with default graticules 7 | args = {"lon_0": 0} 8 | conic_args = {"lon_0":0, 9 | "lat_0": -10, 10 | "lat_1": -40, 11 | "lat_2": 10 12 | } 13 | 14 | for name, proj_cls in skm.projection_register.items(): 15 | proj = None 16 | try: 17 | proj = proj_cls(**args) 18 | except TypeError: 19 | try: 20 | proj = proj_cls(**conic_args) 21 | except TypeError: 22 | pass 23 | 24 | if proj is not None: 25 | map = skm.Map(proj, interactive=False) 26 | map.grid() 27 | map.title(name) 28 | 29 | plt.show() 30 | -------------------------------------------------------------------------------- /examples/example1.py: -------------------------------------------------------------------------------- 1 | # load projection and helper functions 2 | import numpy as np 3 | import skymapper as skm 4 | import matplotlib.pyplot as plt 5 | 6 | def getCatalog(size=10000, survey=None): 7 | # dummy catalog: uniform on sphere 8 | # Marsaglia (1972) 9 | xyz = np.random.normal(size=(size, 3)) 10 | r = np.sqrt((xyz**2).sum(axis=1)) 11 | dec = np.arccos(xyz[:,2]/r) / skm.DEG2RAD - 90 12 | ra = 180 - np.arctan2(xyz[:,0], xyz[:,1]) / skm.DEG2RAD 13 | 14 | # survey selection 15 | if survey is not None: 16 | inside = survey.contains(ra, dec) 17 | ra = ra[inside] 18 | dec = dec[inside] 19 | return ra, dec 20 | 21 | def makeHealpixMap(ra, dec, nside=1024, nest=False): 22 | # convert a ra/dec catalog into healpix map with counts per cell 23 | import healpy as hp 24 | ipix = hp.ang2pix(nside, ra, dec, nest=nest, lonlat=True) 25 | return np.bincount(ipix, minlength=hp.nside2npix(nside)) 26 | 27 | class TestSurvey(skm.survey.Survey): 28 | def contains(self, ra, dec): 29 | # simplistic DES like survey 30 | return (dec < 5) & (dec > -60) & ((ra < 90) | (ra > 300)) 31 | 32 | 33 | if __name__ == "__main__": 34 | 35 | # load RA/Dec from catalog 36 | size = 100000 37 | try: 38 | from skymapper.survey import DES 39 | survey = DES() 40 | except ImportError: 41 | survey = TestSurvey() 42 | ra, dec = getCatalog(size, survey=survey) 43 | 44 | # define the best Albers projection for the footprint 45 | # minimizing the variation in distortion 46 | # alternatively, specify the projection, e.g. proj = skm.Hammer(0) 47 | crit = skm.stdDistortion 48 | proj = skm.Albers.optimize(ra, dec, crit=crit) 49 | 50 | # construct map: 51 | # if fig axis is provided, will use it; otherwise will create figure 52 | # the outline of the map can be styled with kwargs for matplotlib Polygon 53 | fig = plt.figure(tight_layout=True, figsize=(10, 6)) 54 | ax = fig.add_subplot(111, aspect='equal') 55 | map = skm.Map(proj, ax=ax) 56 | 57 | # add graticules, separated by 15 deg 58 | # the lines can be styled with kwargs for matplotlib Line2D 59 | # additional arguments for formatting the graticule labels 60 | sep=15 61 | map.grid(sep=sep) 62 | 63 | #### 1. plot density in healpix cells #### 64 | nside = 32 65 | mappable = map.density(ra, dec, nside=nside) 66 | cb = map.colorbar(mappable, cb_label="$n$ [arcmin$^{-2}$]") 67 | 68 | # add random scatter plot 69 | nsamples = 30 70 | size = 100*np.random.rand(nsamples) 71 | map.scatter(ra[:nsamples], dec[:nsamples], s=size, edgecolor='w', facecolor='None') 72 | 73 | # focus on relevant region 74 | map.focus(ra, dec) 75 | 76 | # entitle: access mpl figure 77 | map.title('Density with random scatter') 78 | 79 | # save 80 | map.savefig("skymapper-example1.png") 81 | 82 | #### 2. plot healpix map #### 83 | # clone the map without data contents 84 | map2 = map.clone() 85 | 86 | # to make healpix map, simply bin the counts of ra/dec 87 | m = makeHealpixMap(ra, dec, nside=nside) 88 | m = np.ma.array(m, mask=(m==0)) 89 | mappable2 = map2.healpix(m, cmap="YlOrRd") 90 | cb2 = map2.colorbar(mappable2, cb_label="Healpix cell count") 91 | map2.title('Healpix map') 92 | 93 | #### 3. show map distortion over the survey #### 94 | # compute distortion for all ra, dec and make hexbin plot 95 | map3 = map.clone() 96 | a,b = proj.distortion(ra, dec) 97 | mappable3 = map3.hexbin(ra, dec, C=1-np.abs(b/a), vmin=0, vmax=0.3, cmap='RdYlBu_r') 98 | cb3 = map3.colorbar(mappable3, cb_label='Distortion') 99 | map3.title('Projection distortion') 100 | 101 | #### 4. extrapolate sampled values over all sky with an all-sky projection #### 102 | proj4 = skm.McBrydeThomasFPQ(0) 103 | map4 = skm.Map(proj4) 104 | 105 | # show with 45 deg graticules 106 | sep=45 107 | map4.grid(sep=sep) 108 | 109 | # alter number of labels at the south pole 110 | map4.labelMeridiansAtParallel(-90, size=8) 111 | map4.labelMeridiansAtParallel(0, color='w') 112 | 113 | # as example: extrapolate declination over sky 114 | # this is slow when working with lots of samples... 115 | mappable4 = map4.extrapolate(ra[::10], dec[::10], dec[::10], resolution=100, cmap='Spectral', vmax=90, vmin=-90) 116 | cb4 = map4.colorbar(mappable4, cb_label='Dec') 117 | 118 | # add footprint shade 119 | nside = 32 120 | footprint4 = map4.footprint(survey, nside=nside, zorder=20, facecolors='w', alpha=0.3) 121 | map4.title('Extrapolation on the sphere') 122 | 123 | # when run as a script: need to show the result 124 | plt.show() -------------------------------------------------------------------------------- /examples/optimize_projection.py: -------------------------------------------------------------------------------- 1 | # load projection and helper functions 2 | import numpy as np 3 | import skymapper as skm 4 | import matplotlib.pyplot as plt 5 | 6 | def getCatalog(size=10000, survey=None): 7 | # dummy catalog: uniform on sphere 8 | # Marsaglia (1972) 9 | xyz = np.random.normal(size=(size, 3)) 10 | r = np.sqrt((xyz**2).sum(axis=1)) 11 | dec = np.arccos(xyz[:,2]/r) / skm.DEG2RAD - 90 12 | ra = 180 - np.arctan2(xyz[:,0], xyz[:,1]) / skm.DEG2RAD 13 | 14 | if survey is not None: 15 | inside = survey.contains(ra, dec) 16 | ra = ra[inside] 17 | dec = dec[inside] 18 | 19 | return ra, dec 20 | 21 | class TestSurvey(skm.survey.Survey): 22 | def contains(self, ra, dec): 23 | # simplistic DES like survey 24 | return (dec < 5) & (dec > -60) & ((ra < 90) | (ra > 300)) 25 | 26 | if __name__ == "__main__": 27 | 28 | # load RA/Dec from catalog 29 | size = 10000 30 | try: 31 | from skymapper.survey import DES 32 | 33 | survey = DES() 34 | except ImportError: 35 | survey = TestSurvey() 36 | ra, dec = getCatalog(size, survey=survey) 37 | 38 | # define the best Albers projection for the footprint 39 | # minimizing the variation in distortion, aka ellipticity 40 | for crit in [skm.meanDistortion, skm.maxDistortion, skm.stdDistortion]: 41 | proj = skm.Albers.optimize(ra, dec, crit) 42 | map = skm.Map(proj) 43 | map.grid() 44 | map.labelMeridiansAtParallel(0, meridians=[]) 45 | map.footprint(survey, nside=64, zorder=20, facecolor='k', alpha=0.5) 46 | a,b = proj.distortion(ra, dec) 47 | c = map.extrapolate(ra, dec, 1-np.abs(b/a), cmap='coolwarm', vmin=0, vmax=0.3, resolution=72) 48 | cb = map.colorbar(c, cb_label='distortion') 49 | map.focus(ra, dec) 50 | map.title(proj.__class__.__name__ + ": " + crit.__name__) 51 | 52 | plt.show() 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | long_description = open('README.md').read() 4 | 5 | setup( 6 | name="skymapper", 7 | description="Mapping astronomical survey data on the sky, handsomely", 8 | long_description=long_description, 9 | long_description_content_type='text/markdown', 10 | version="0.4.7", 11 | license="MIT", 12 | author="Peter Melchior", 13 | author_email="peter.m.melchior@gmail.com", 14 | url="https://github.com/pmelchior/skymapper", 15 | packages=["skymapper", "skymapper.survey"], 16 | package_data={"skymapper.survey": ['*.ply']}, 17 | classifiers=[ 18 | "Development Status :: 4 - Beta", 19 | "License :: OSI Approved :: MIT License", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Education", 22 | "Intended Audience :: Science/Research", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python", 25 | "Topic :: Scientific/Engineering :: Astronomy" 26 | ], 27 | keywords = ['visualization','map projection','matplotlib'], 28 | install_requires=["matplotlib", "numpy", "scipy", "healpy"] 29 | ) 30 | -------------------------------------------------------------------------------- /skymapper/__init__.py: -------------------------------------------------------------------------------- 1 | # decorator for registering the survey footprint loaders and projections 2 | projection_register = {} 3 | def register_projection(cls): 4 | projection_register[cls.__name__] = cls 5 | 6 | survey_register = {} 7 | def register_survey(cls): 8 | survey_register[cls.__name__] = cls 9 | 10 | # [blatant copy from six to avoid dependency] 11 | # python 2 and 3 compatible metaclasses 12 | # see http://python-future.org/compatible_idioms.html#metaclasses 13 | 14 | class Meta(type): 15 | def __new__(meta, name, bases, class_dict): 16 | cls = type.__new__(meta, name, bases, class_dict) 17 | 18 | # remove those that are directly derived from BaseProjection 19 | if BaseProjection not in bases: 20 | register_projection(cls) 21 | 22 | return cls 23 | 24 | def with_metaclass(meta, *bases): 25 | """Create a base class with a metaclass.""" 26 | # This requires a bit of explanation: the basic idea is to make a dummy 27 | # metaclass for one level of class instantiation that replaces itself with 28 | # the actual metaclass. 29 | class metaclass(type): 30 | 31 | def __new__(cls, name, this_bases, d): 32 | return meta(name, bases, d) 33 | 34 | @classmethod 35 | def __prepare__(cls, name, this_bases): 36 | return meta.__prepare__(name, bases) 37 | return type.__new__(metaclass, 'temporary_class', (), {}) 38 | 39 | from .map import * 40 | from .projection import * 41 | from . import survey 42 | -------------------------------------------------------------------------------- /skymapper/healpix.py: -------------------------------------------------------------------------------- 1 | import healpy as hp 2 | import numpy as np 3 | 4 | # python 3 compatible 5 | try: 6 | xrange 7 | except NameError: 8 | xrange = range 9 | 10 | def getHealpixArea(nside): 11 | return hp.nside2pixarea(nside, degrees=True) 12 | 13 | def getHealpixVertices(pixels, nside, nest=False): 14 | """Get polygon vertices for list of HealPix pixels. 15 | 16 | Args: 17 | pixels: list of HealPix pixels 18 | nside: HealPix nside 19 | nest: HealPix nesting scheme 20 | 21 | Returns: 22 | vertices: (N,4,2), RA/Dec coordinates of 4 boundary points of cell 23 | """ 24 | corners = np.transpose(hp.boundaries(nside, pixels, step=1, nest=nest), (0, 2, 1)) 25 | corners_x = corners[:, :, 0].flatten() 26 | corners_y = corners[:, :, 1].flatten() 27 | corners_z = corners[:, :, 2].flatten() 28 | vertices_lon, vertices_lat = hp.rotator.vec2dir(corners_x, corners_y, corners_z, lonlat=True) 29 | return np.stack([vertices_lon.reshape(-1, 4), vertices_lat.reshape(-1, 4)], axis=-1) 30 | 31 | 32 | def getGrid(nside, nest=False, return_vertices=False): 33 | pixels = np.arange(hp.nside2npix(nside)) 34 | lon, lat = hp.pix2ang(nside, pixels, nest=nest, lonlat=True) 35 | if return_vertices: 36 | vertices = getHealpixVertices(pixels, nside, nest=nest) 37 | return pixels, lon, lat, vertices 38 | return pixels, lon, lat 39 | 40 | def getCountAtLocations(lon, lat, nside=512, nest=False, per_area=True, return_vertices=False): 41 | """Get number density of objects from lon, lat in HealPix cells. 42 | 43 | Args: 44 | lon: list of longitudes in degrees 45 | lat: list of latutude in degrees 46 | nside: HealPix nside 47 | nest: Healpix NEST scheme 48 | per_area: return counts in units of 1/arcmin^2 49 | return_vertices: whether to also return the boundaries of HealPix cells 50 | 51 | Returns: 52 | bc, [vertices] 53 | bc: count of objects in a HealPix cell if count > 0 54 | vertices: (N,4,2), RA/Dec coordinates of 4 boundary points of cell 55 | """ 56 | # get healpix pixels 57 | ipix = hp.ang2pix(nside, lon, lat, lonlat=True, nest=nest) 58 | # count how often each pixel is hit 59 | bc = np.bincount(ipix, minlength=hp.nside2npix(nside)) 60 | if per_area: 61 | bc = bc.astype('f8') 62 | bc /= hp.nside2resol(nside, arcmin=True)**2 # in arcmin^-2 63 | 64 | # for every non-empty pixel: get the vertices that confine it 65 | if return_vertices: 66 | pixels = np.nonzero(bc)[0] 67 | vertices = getHealpixVertices(pixels, nside) 68 | return bc, vertices 69 | return bc 70 | 71 | def reduceAtLocations(lon, lat, value, reduce_fct=np.mean, nside=512, nest=False, return_vertices=False): 72 | """Reduce values at given lon, lat in HealPix cells to a scalar. 73 | 74 | Args: 75 | lon: list of longitudes in degrees 76 | lat: list of latutude in degrees 77 | value: list of values to be reduced 78 | reduce_fct: function to operate on values in each cell 79 | nside: HealPix nside 80 | nest: Healpix NEST scheme 81 | return_vertices: whether to also return the boundaries of HealPix cells 82 | 83 | Returns: 84 | v, [vertices] 85 | v: reduction of values in a HealPix cell if count > 0, otherwise masked 86 | vertices: (N,4,2), RA/Dec coordinates of 4 boundary points of cell 87 | """ 88 | # get healpix pixels 89 | ipix = hp.ang2pix(nside, lon, lat, lonlat=True, nest=nest) 90 | # count how often each pixel is hit, only use non-empty pixels 91 | bc = np.bincount(ipix, minlength=hp.nside2npix(nside)) 92 | pixels = np.nonzero(bc)[0] 93 | 94 | v = np.ma.empty(bc.size, mask=(bc==0)) 95 | for pixel in pixels: 96 | sel = (ipix == pixels) 97 | v.data[pixel] = reduce_fct(value[sel]) 98 | 99 | # get the vertices that confine each pixel 100 | # convert to lon, lat (thanks to Eric Huff) 101 | if return_vertices: 102 | vertices = getHealpixVertices(pixels, nside) 103 | return v, vertices 104 | return v 105 | -------------------------------------------------------------------------------- /skymapper/map.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | import matplotlib.pyplot 3 | import numpy as np 4 | import math, re, pickle 5 | import scipy.interpolate 6 | from . import healpix 7 | from matplotlib.lines import Line2D 8 | from matplotlib.patches import Polygon 9 | 10 | 11 | DEG2RAD = np.pi/180 12 | 13 | def angularDistance(radec1, radec2): 14 | """Compute distance on the curved sky""" 15 | ra1, dec1 = radec1 * DEG2RAD 16 | ra2, dec2 = radec2 * DEG2RAD 17 | return np.arccos(np.sin(dec1)*np.sin(dec2) + np.cos(dec1)*np.cos(dec2)*np.cos(ra1-ra2)) 18 | 19 | 20 | # extrapolation function from 21 | # http://stackoverflow.com/questions/2745329/how-to-make-scipy-interpolate-give-an-extrapolated-result-beyond-the-input-range 22 | # improved to order x and y to have ascending x 23 | def extrap(x, xp, yp): 24 | """np.interp function with linear extrapolation""" 25 | x_ = np.array(x) 26 | order = np.argsort(xp) 27 | xp_ = xp[order] 28 | yp_ = yp[order] 29 | 30 | y = np.array(np.interp(x_, xp_, yp_)) 31 | y[x_ < xp_[0]] = yp_[0] + (x_[x_ < xp_[0]] -xp_[0]) * (yp_[0] - yp_[1]) / (xp_[0] - xp_[1]) 32 | y[x_ > xp_[-1]] = yp_[-1] + (x_[x_ > xp_[-1]] -xp_[-1])*(yp_[-1] - yp_[-2])/(xp_[-1] - xp_[-2]) 33 | return y 34 | 35 | def degFormatter(deg): 36 | """Formats degrees as X˚ 37 | 38 | Args: 39 | deg: float 40 | Returns: 41 | string 42 | """ 43 | return "${:d}^\circ$".format(int(deg)) 44 | 45 | def degPMFormatter(deg): 46 | """Formats degrees as [+-]X˚ 47 | 48 | Args: 49 | deg: float 50 | 51 | Return: 52 | String 53 | """ 54 | if deg == 0: 55 | return degFormatter(deg) 56 | return "${:+d}^\circ$".format(int(deg)) 57 | 58 | def deg360Formatter(deg): 59 | """Formats degrees as X˚ with X in [0..360] 60 | 61 | Args: 62 | deg: float 63 | Returns: 64 | string 65 | """ 66 | if deg < 0: 67 | deg += 360 68 | return degFormatter(deg) 69 | 70 | def deg180Formatter(deg): 71 | """Formats degrees as X˚ with X in [-180..180] 72 | 73 | Args: 74 | deg: float 75 | Returns: 76 | string 77 | """ 78 | if deg < -180: 79 | deg += 360 80 | if deg > 180: 81 | deg -= 360 82 | return degPMFormatter(deg) 83 | 84 | def hourAngleFormatter(ra): 85 | """String formatter for "hh:mm" 86 | 87 | Args: 88 | deg: float 89 | 90 | Return: 91 | String 92 | """ 93 | if ra < 0: 94 | ra += 360 95 | hours = int(ra//15) 96 | minutes = int(float(ra - hours*15)/15 * 60) 97 | if minutes: 98 | return "${:d}^{{{:>02}}}$h".format(hours, minutes) 99 | return "${:d}$h".format(hours) 100 | 101 | def degEastWestFormatter(deg): 102 | """Formats degrees as X˚D with X in [0..180] and D in ['E','W'] 103 | 104 | Args: 105 | deg: float 106 | Returns: 107 | string 108 | """ 109 | if deg < -180: 110 | deg += 360 111 | if deg > 180: 112 | deg -= 360 113 | 114 | d = '' 115 | if deg > 0: 116 | d = 'E' 117 | if deg < 0: 118 | deg *= -1 119 | d = 'W' 120 | return degFormatter(deg) + d 121 | 122 | def nullFormatter(deg): 123 | return "" 124 | 125 | 126 | def _parseArgs(locals): 127 | """Turn list of arguments (all named or kwargs) into flat dictionary""" 128 | locals.pop('self') 129 | kwargs = locals.pop('kwargs', {}) 130 | for k,v in kwargs.items(): 131 | locals[k] = v 132 | return locals 133 | 134 | class Map(): 135 | def __init__(self, proj, ax=None, interactive=True, **kwargs): 136 | """Create Map with a given projection. 137 | 138 | A `skymapper.Map` holds a predefined projection and `matplotlib` axes 139 | and figures to enable plotting on the sphere with proper labeling 140 | and inter/extrapolations. 141 | 142 | It also allows for interactive and exploratory work by updateing the 143 | maps after pan/zoom events. 144 | 145 | Most of the methods are wrappers of `matplotlib` functions by the same 146 | names, so that one can mostly interact with a `Map` instance as one 147 | would do with a `matplotlib.axes`. 148 | 149 | For plotting purposes, it is recommended to switch `interactive` off. 150 | 151 | Args: 152 | proj: `skymapper.Projection` instance 153 | ax: `matplotlib.axes` instance, will be created otherwise 154 | interactive: if pan/zoom is enabled for map updates 155 | **kwargs: styling of the `matplotlib.patches.Polygon` that shows 156 | the outline of the map. 157 | """ 158 | # store arguments to regenerate the map 159 | self._config = {'__init__': _parseArgs(locals())} 160 | self.proj = proj 161 | self._setFigureAx(ax, interactive=interactive) 162 | self._resolution = 75 # for graticules 163 | self._setEdge(**kwargs) 164 | self.ax.relim() 165 | self.ax.autoscale_view() 166 | self._setFrame() 167 | 168 | def _setFigureAx(self, ax=None, interactive=True): 169 | if ax is None: 170 | self.fig = matplotlib.pyplot.figure(tight_layout=True) 171 | self.ax = self.fig.add_subplot(111, aspect='equal') 172 | else: 173 | self.ax = ax 174 | self.ax.set_aspect('equal') 175 | self.fig = self.ax.get_figure() 176 | self.fig.set_tight_layout(True) 177 | 178 | self.ax.spines['left'].set_visible(False) 179 | self.ax.spines['right'].set_visible(False) 180 | self.ax.spines['top'].set_visible(False) 181 | self.ax.spines['bottom'].set_visible(False) 182 | self.ax.xaxis.set_visible(False) 183 | self.ax.yaxis.set_visible(False) 184 | self.ax.tick_params(axis='both', which='both', length=0) 185 | self.ax.xaxis.set_ticks_position('none') 186 | self.ax.yaxis.set_ticks_position('none') 187 | 188 | # attach event handlers 189 | if interactive: 190 | self.fig.show() 191 | self._press_evt = self.fig.canvas.mpl_connect('button_press_event', self._pressHandler) 192 | self._release_evt = self.fig.canvas.mpl_connect('button_release_event', self._releaseHandler) 193 | self._scroll_evt = self.fig.canvas.mpl_connect('scroll_event', self._scrollHandler) 194 | 195 | def clone(self, ax=None): 196 | """Clone map 197 | 198 | Args: 199 | ax: `matplotlib.axes` instance, will be created otherwise 200 | Returns: 201 | New map using the same projections and configuration 202 | """ 203 | config = dict(self._config) 204 | config['xlim'] = self.ax.get_xlim() 205 | config['ylim'] = self.ax.get_ylim() 206 | return Map._create(config, ax=ax) 207 | 208 | def save(self, filename): 209 | """Save map configuration to file 210 | 211 | All aspects necessary to reproduce a map are stored in a pickle file. 212 | 213 | Args: 214 | filename: name for pickle file 215 | 216 | Returns: 217 | None 218 | """ 219 | try: 220 | with open(filename, 'wb') as fp: 221 | config = dict(self._config) 222 | config['xlim'] = self.ax.get_xlim() 223 | config['ylim'] = self.ax.get_ylim() 224 | pickle.dump(config, fp) 225 | except IOError as e: 226 | raise 227 | 228 | @staticmethod 229 | def load(filename, ax=None): 230 | """Load map from pickled file 231 | 232 | Args: 233 | filename: name for pickle file 234 | ax: `matplotlib.axes` instance, will be created otherwise 235 | Returns: 236 | `skymapper.Map` 237 | """ 238 | try: 239 | with open(filename, 'rb') as fp: 240 | config = pickle.load(fp) 241 | fp.close() 242 | return Map._create(config, ax=ax) 243 | except IOError as e: 244 | raise 245 | 246 | @staticmethod 247 | def _create(config, ax=None): 248 | init_args = config.pop('__init__') 249 | init_args['ax'] = ax 250 | map = Map(**init_args) 251 | 252 | xlim = config.pop('xlim') 253 | ylim = config.pop('ylim') 254 | map.ax.set_xlim(xlim) 255 | map.ax.set_ylim(ylim) 256 | map._setFrame() 257 | 258 | meridian_args = config.pop('labelMeridiansAtParallel', {}) 259 | parallel_args = config.pop('labelParallelsAtMeridian', {}) 260 | for method in config.keys(): 261 | getattr(map, method)(**config[method]) 262 | 263 | for args in meridian_args.values(): 264 | map.labelMeridiansAtParallel(**args) 265 | 266 | for args in parallel_args.values(): 267 | map.labelParallelsAtMeridian(**args) 268 | 269 | return map 270 | 271 | @property 272 | def parallels(self): 273 | """Get the location of the drawn parallels""" 274 | return [ float(m.group(1)) for c,m in self.artists(r'grid-parallel-([\-\+0-9.]+)', regex=True) ] 275 | 276 | @property 277 | def meridians(self): 278 | """Get the location of the drawn meridians""" 279 | return [ float(m.group(1)) for c,m in self.artists(r'grid-meridian-([\-\+0-9.]+)', regex=True) ] 280 | 281 | def artists(self, gid, regex=False): 282 | """Get the `matplotlib` artists used in the map 283 | 284 | Args: 285 | gid: `gid` string of the artist 286 | regex: if regex matching is done 287 | 288 | Returns: 289 | list of matching artists 290 | if `regex==True`, returns list of (artist, match) 291 | """ 292 | if regex: 293 | matches = [ re.match(gid, c.get_gid()) if c.get_gid() is not None else None for c in self.ax.get_children() ] 294 | return [ (c,m) for c,m in zip(self.ax.get_children(), matches) if m is not None ] 295 | else: # direct match 296 | return [ c for c in self.ax.get_children() if c.get_gid() is not None and c.get_gid().find(gid) != -1 ] 297 | 298 | def _getParallel(self, p, reverse=False): 299 | if not reverse: 300 | return self.proj.transform(self._lon_range, p*np.ones(len(self._lon_range))) 301 | return self.proj.transform(self._lon_range[::-1], p*np.ones(len(self._lon_range))) 302 | 303 | def _getMeridian(self, m, reverse=False): 304 | if not reverse: 305 | return self.proj.transform(m*np.ones(len(self._lat_range)), self._lat_range) 306 | return self.proj.transform(m*np.ones(len(self._lat_range)), self._lat_range[::-1]) 307 | 308 | def _setParallel(self, p, **kwargs): 309 | x, y = self._getParallel(p) 310 | artist = Line2D(x, y, **kwargs) 311 | self.ax.add_line(artist) 312 | return artist 313 | 314 | def _setMeridian(self, m, **kwargs): 315 | x, y = self._getMeridian(m) 316 | artist = Line2D(x, y, **kwargs) 317 | self.ax.add_line(artist) 318 | return artist 319 | 320 | def _setEdge(self, **kwargs): 321 | self._lat_range = np.linspace(-89.9999, 89.9999, self._resolution) 322 | self._lon_range = np.linspace(-179.9999, 179.9999, self._resolution) + self.proj.lon_0 323 | 324 | 325 | # styling: frame needs to be on top of everything, must be transparent 326 | facecolor = 'None' 327 | zorder = 1000 328 | lw = kwargs.pop('lw', 0.7) 329 | edgecolor = kwargs.pop('edgecolor', 'k') 330 | # if there is facecolor: clone the polygon and put it in as bottom layer 331 | facecolor_ = kwargs.pop('facecolor', '#dddddd') 332 | 333 | # polygon of the map edge: top, left, bottom, right 334 | # don't draw poles if that's a single point 335 | lines = [] 336 | if not self.proj.poleIsPoint[90]: 337 | lines.append(self._getParallel(90)) 338 | lines.append(self._getMeridian(self._lon_range[-1], reverse=True)) 339 | if not self.proj.poleIsPoint[-90]: 340 | lines.append(self._getParallel(-90, reverse=True)) 341 | lines.append(self._getMeridian(self._lon_range[0])) 342 | xy = np.concatenate(lines, axis=1).T 343 | self._edge = Polygon(xy, closed=True, edgecolor=edgecolor, facecolor=facecolor, lw=lw, zorder=zorder,gid="edge", **kwargs) 344 | self.ax.add_patch(self._edge) 345 | 346 | if facecolor_ is not None: 347 | zorder = -1000 348 | edgecolor = 'None' 349 | poly = Polygon(xy, closed=True, edgecolor=edgecolor, facecolor=facecolor_, zorder=zorder, gid="edge-background") 350 | self.ax.add_patch(poly) 351 | 352 | def contains(self, x, y): 353 | xy = np.dstack((x,y)).reshape(-1,2) 354 | return self._edge.get_path().contains_points(xy).reshape(x.shape) 355 | 356 | def xlim(self, left=None, right=None, **kw): 357 | """Get/set the map limits in x-direction""" 358 | if left is None and right is None: 359 | return (self._edge.xy[:, 0].min(), self._edge.xy[:, 0].max()) 360 | else: 361 | self.ax.set_xlim(left=left, right=right, **kw) 362 | self._resetFrame() 363 | 364 | def ylim(self, bottom=None, top=None, **kw): 365 | """Get/set the map limits in x-direction""" 366 | if bottom is None and top is None: 367 | return (self._edge.xy[:, 1].min(), self._edge.xy[:, 1].max()) 368 | else: 369 | self.ax.set_ylim(bottom=bottom, top=top, **kw) 370 | self._resetFrame() 371 | 372 | def grid(self, sep=30, parallel_fmt=None, meridian_fmt=None, lat_min=-90, lat_max=90, lon_min=-180, lon_max=180, **kwargs): 373 | """Set map grid / graticules 374 | 375 | Args: 376 | sep: distance between graticules in deg. if two-element list, distance in lat and long, resp. 377 | parallel_fmt: formatter for parallel labels 378 | meridian_fmt: formatter for meridian labels 379 | lat_min: minimum latitude for graticules 380 | lat_max: maximum latitude for graticules 381 | lon_min: minimum longitude for graticules 382 | lon_max: maximum longitude for graticules 383 | **kwargs: styling of `matplotlib.lines.Line2D` for the graticules 384 | """ 385 | try: 386 | if len(sep)==2: 387 | sep_lat, sep_lon = sep 388 | except TypeError: 389 | sep_lat = sep_lon = sep 390 | 391 | # clone() sets sep_lat, sep_lon, need to get them from kwargs 392 | # also need to avoid sep being stored in config 393 | del sep 394 | sep_lat = kwargs.pop("sep_lat", sep_lat) 395 | sep_lon = kwargs.pop("sep_lon", sep_lon) 396 | 397 | if parallel_fmt is None: 398 | parallel_fmt = degPMFormatter 399 | if meridian_fmt is None: 400 | if self.proj.lon_type == "ra": 401 | meridian_fmt = deg360Formatter 402 | else: 403 | meridian_fmt = deg180Formatter 404 | self._config['grid'] = _parseArgs(locals()) 405 | self._lat_range = np.linspace(lat_min, lat_max, self._resolution) 406 | self._lon_range = np.linspace(lon_min, lon_max, self._resolution) + self.proj.lon_0 407 | _parallels = np.arange(-90+sep_lat,90,sep_lat) 408 | if self.proj.lon_0 % sep_lon == 0: 409 | _meridians = np.arange(sep_lon * ((self.proj.lon_0 + 180) // sep_lon - 1), sep_lon * ((self.proj.lon_0 - 180) // sep_lon), -sep_lon) 410 | else: 411 | _meridians = np.arange(sep_lon * ((self.proj.lon_0 + 180) // sep_lon), sep_lon * ((self.proj.lon_0 - 180) // sep_lon), -sep_lon) 412 | _meridians[_meridians < 0] += 360 413 | _meridians[_meridians >= 360] -= 360 414 | 415 | # clean up previous grid 416 | artists = self.artists('grid-meridian') + self.artists('grid-parallel') 417 | for artist in artists: 418 | artist.remove() 419 | 420 | # clean up frame meridian and parallel labels because they're tied to the grid 421 | artists = self.artists('meridian-label') + self.artists('parallel-label') 422 | for artist in artists: 423 | artist.remove() 424 | 425 | # styling: based on edge 426 | ls = kwargs.pop('ls', '-') 427 | lw = kwargs.pop('lw', self._edge.get_linewidth() / 2) 428 | c = kwargs.pop('c', self._edge.get_edgecolor()) 429 | alpha = kwargs.pop('alpha', 0.2) 430 | zorder = kwargs.pop('zorder', self._edge.get_zorder() - 1) 431 | 432 | for p in _parallels: 433 | self._setParallel(p, gid='grid-parallel-%r' % p, lw=lw, c=c, alpha=alpha, zorder=zorder, **kwargs) 434 | for m in _meridians: 435 | self._setMeridian(m, gid='grid-meridian-%r' % m, lw=lw, c=c, alpha=alpha, zorder=zorder, **kwargs) 436 | 437 | # (re)generate the frame labels 438 | for method in ['labelMeridiansAtFrame', 'labelParallelsAtFrame']: 439 | if method in self._config.keys(): 440 | getattr(self, method)(**self._config[method]) 441 | else: 442 | getattr(self, method)() 443 | 444 | # (re)generate edge labels 445 | for method in ['labelMeridiansAtParallel', 'labelParallelsAtMeridian']: 446 | if method in self._config.keys(): 447 | args_list = self._config.pop(method, []) 448 | for args in args_list.values(): 449 | getattr(self, method)(**args) 450 | else: 451 | # label meridians: at the poles if they are not points 452 | if method == 'labelMeridiansAtParallel': 453 | # determine the parallel that has the most space for labels 454 | dec = [-90, 0, 90] 455 | ra = [self.proj.lon_0,] * 3 456 | jac = np.sum(self.proj.gradient(ra, dec)**2, axis=1) 457 | p = dec[np.argmax(jac)] 458 | getattr(self, method)(p) 459 | # label both outer meridians 460 | else: 461 | degs = [self.proj.lon_0 + 180, self.proj.lon_0 - 180] 462 | for deg in degs: 463 | getattr(self, method)(deg) 464 | 465 | def _negateLoc(self, loc): 466 | if loc == "bottom": 467 | return "top" 468 | if loc == "top": 469 | return "bottom" 470 | if loc == "left": 471 | return "right" 472 | if loc == "right": 473 | return "left" 474 | 475 | def labelMeridiansAtParallel(self, p, loc=None, meridians=None, pad=None, direction='parallel', fmt=None, 476 | clear_existing_labels=False, **kwargs): 477 | """Label the meridians intersecting a given parallel 478 | 479 | The method is called by `grid()` but can be used to overwrite the defaults. 480 | 481 | Args: 482 | p: parallel in deg 483 | loc: location of the label with respect to `p`, from `['top', 'bottom']` 484 | meridians: list of meridians to label, if None labels all of them 485 | pad: padding of annotation, in units of fontsize 486 | direction: tangent of the label, from `['parallel', 'meridian']` 487 | fmt: formatter for labels, if `None` use default 488 | clear_existing_labels : bool (default=False) 489 | **kwargs: styling of `matplotlib` annotations for the graticule labels 490 | """ 491 | arguments = _parseArgs(locals()) 492 | 493 | if p in self.proj.poleIsPoint.keys() and self.proj.poleIsPoint[p]: 494 | return 495 | 496 | myname = 'labelMeridiansAtParallel' 497 | if myname not in self._config.keys(): 498 | self._config[myname] = dict() 499 | 500 | # remove all existing labels: 501 | if clear_existing_labels: 502 | for _p in self._config[myname].keys(): 503 | gid = 'meridian-label-%r' % _p 504 | artists = self.artists(gid) 505 | for artist in artists: 506 | artist.remove() 507 | 508 | # remove exisiting labels at p 509 | gid = 'meridian-label-%r' % p 510 | if p in self._config[myname].keys(): 511 | artists = self.artists(gid) 512 | for artist in artists: 513 | artist.remove() 514 | 515 | self._config[myname][p] = arguments 516 | 517 | if meridians is None: 518 | meridians = self.meridians 519 | 520 | # determine rot_base so that central label is upright 521 | rotation = kwargs.pop('rotation', None) 522 | if rotation is None or loc is None: 523 | m = self.proj.lon_0 524 | dxy = self.proj.gradient(m, p, direction=direction) 525 | angle = np.arctan2(*dxy) / DEG2RAD 526 | options = [-90, 90] 527 | closest = np.argmin(np.abs(options - angle)) 528 | rot_base = options[closest] 529 | 530 | if loc is None: 531 | if p >= 0: 532 | loc = 'top' 533 | else: 534 | loc = 'bottom' 535 | assert loc in ['top', 'bottom'] 536 | 537 | horizontalalignment = kwargs.pop('horizontalalignment', 'center') 538 | verticalalignment = kwargs.pop('verticalalignment', self._negateLoc(loc)) 539 | zorder = kwargs.pop('zorder', 20) 540 | size = kwargs.pop('size', matplotlib.rcParams['font.size']) 541 | # styling consistent with frame, i.e. with edge 542 | color = kwargs.pop('color', self._edge.get_edgecolor()) 543 | alpha = kwargs.pop('alpha', self._edge.get_alpha()) 544 | zorder = kwargs.pop('zorder', self._edge.get_zorder() + 1) # on top of edge 545 | if fmt is None: 546 | fmt = self._config['grid']['meridian_fmt'] 547 | 548 | if pad is None: 549 | pad = size / 3 550 | 551 | for m in meridians: 552 | # move label along meridian 553 | xp, yp = self.proj(m, p) 554 | dxy = self.proj.gradient(m, p, direction="meridian") 555 | dxy *= pad / np.sqrt((dxy**2).sum()) 556 | if loc == 'bottom': # dxy in positive RA 557 | dxy *= -1 558 | 559 | if rotation is None: 560 | dxy_ = self.proj.gradient(m, p, direction=direction) 561 | angle = rot_base - np.arctan2(*dxy_) / DEG2RAD 562 | else: 563 | angle = rotation 564 | 565 | self.ax.annotate(fmt(m), (xp, yp), xytext=dxy, textcoords='offset points', rotation=angle, rotation_mode='anchor', horizontalalignment=horizontalalignment, verticalalignment=verticalalignment, size=size, color=color, alpha=alpha, zorder=zorder, gid=gid, in_layout=False, **kwargs) 566 | 567 | def labelParallelsAtMeridian(self, m, loc=None, parallels=None, pad=None, direction='parallel', fmt=None, 568 | clear_existing_labels=False, **kwargs): 569 | """Label the parallel intersecting a given meridian 570 | 571 | The method is called by `grid()` but can be used to overwrite the defaults. 572 | 573 | Args: 574 | m: meridian in deg 575 | loc: location of the label with respect to `m`, from `['left', 'right']` 576 | parallel: list of parallels to label, if None labels all of them 577 | pad: padding of annotation, in units of fontsize 578 | direction: tangent of the label, from `['parallel', 'meridian']` 579 | fmt: formatter for label, if `None` use default 580 | clear_existing_labels : bool (default=False) 581 | **kwargs: styling of `matplotlib` annotations for the graticule labels 582 | """ 583 | arguments = _parseArgs(locals()) 584 | 585 | myname = 'labelParallelsAtMeridian' 586 | if myname not in self._config.keys(): 587 | self._config[myname] = dict() 588 | 589 | # remove all existing labels 590 | if clear_existing_labels: 591 | for _m in self._config[myname].keys(): 592 | gid = 'parallel-label-%r' % _m 593 | artists = self.artists(gid) 594 | for artist in artists: 595 | artist.remove() 596 | 597 | # remove exisiting labels at m 598 | gid = 'parallel-label-%r' % m 599 | if m in self._config[myname].keys(): 600 | artists = self.artists(gid) 601 | for artist in artists: 602 | artist.remove() 603 | 604 | self._config[myname][m] = arguments 605 | 606 | # determine rot_base so that central label is upright 607 | rotation = kwargs.pop('rotation', None) 608 | if rotation is None or loc is None: 609 | p = 0 610 | dxy = self.proj.gradient(m, p, direction=direction) 611 | angle = np.arctan2(*dxy) / DEG2RAD 612 | options = [-90, 90] 613 | closest = np.argmin(np.abs(options - angle)) 614 | rot_base = options[closest] 615 | 616 | if loc is None: 617 | if m < self.proj.lon_0: # meridians on the left: dx goes in positive RA 618 | dxy *= -1 619 | if dxy[0] > 0: 620 | loc = 'right' 621 | else: 622 | loc = 'left' 623 | assert loc in ['left', 'right'] 624 | 625 | if parallels is None: 626 | parallels = self.parallels 627 | 628 | horizontalalignment = kwargs.pop('horizontalalignment', self._negateLoc(loc)) 629 | verticalalignment = kwargs.pop('verticalalignment', 'center') 630 | zorder = kwargs.pop('zorder', 20) 631 | size = kwargs.pop('size', matplotlib.rcParams['font.size']) 632 | # styling consistent with frame, i.e. with edge 633 | color = kwargs.pop('color', self._edge.get_edgecolor()) 634 | alpha = kwargs.pop('alpha', self._edge.get_alpha()) 635 | zorder = kwargs.pop('zorder', self._edge.get_zorder() + 1) # on top of edge 636 | if fmt is None: 637 | fmt = self._config['grid']['parallel_fmt'] 638 | 639 | if pad is None: 640 | pad = size/2 # more space for horizontal parallels 641 | 642 | for p in parallels: 643 | # move label along parallel 644 | xp, yp = self.proj(m, p) 645 | dxy = self.proj.gradient(m, p, direction="parallel") 646 | dxy *= pad / np.sqrt((dxy**2).sum()) 647 | if m < self.proj.lon_0: # meridians on the left: dx goes in positive RA 648 | dxy *= -1 649 | 650 | if rotation is None: 651 | dxy_ = self.proj.gradient(m, p, direction=direction) 652 | angle = rot_base - np.arctan2(*dxy_) / DEG2RAD 653 | else: 654 | angle = rotation 655 | 656 | self.ax.annotate(fmt(p), (xp, yp), xytext=dxy, textcoords='offset points', rotation=angle, rotation_mode='anchor', horizontalalignment=horizontalalignment, verticalalignment=verticalalignment, size=size, color=color, alpha=alpha, zorder=zorder, gid=gid, in_layout=False, **kwargs) 657 | 658 | 659 | def labelMeridiansAtFrame(self, loc='auto', meridians=None, pad=None, **kwargs): 660 | """Label the meridians on rectangular frame of the map 661 | 662 | If the view only shows a fraction of the map, a segment or an entire 663 | rectangular frame is shown and the graticule labels are moved to outside 664 | that frame. This method is implicitly called, but can be used to overwrite 665 | the defaults. 666 | 667 | Args: 668 | loc: location of the label with respect to frame, from `['top', 'bottom']` 669 | meridians: list of meridians to label, if None labels all of them 670 | pad: padding of annotation, in units of fontsize 671 | **kwargs: styling of `matplotlib` annotations for the graticule labels 672 | """ 673 | assert loc in [None, 'none', 'auto', 'bottom', 'top'] 674 | 675 | arguments = _parseArgs(locals()) 676 | myname = 'labelMeridiansAtFrame' 677 | self._config[myname] = arguments 678 | 679 | # remove existing 680 | self.ax.xaxis.set_visible(False) 681 | frame_artists = self.artists('frame-meridian-label') 682 | for artist in frame_artists: 683 | artist.remove() 684 | 685 | # check if loc has frame, check both frames for auto 686 | locs = {"bottom": 0, "top": 1} 687 | frame_artists = self.artists(r'frame-([a-zA-Z]+)', regex=True) 688 | frame_locs = [match.group(1) for c,match in frame_artists] 689 | if loc == "auto": 690 | loc = None 691 | xdelta = 0 692 | for c,match in frame_artists: 693 | if match.group(1) in locs.keys(): 694 | xdata = c.get_xdata() 695 | xdelta_ = xdata[-1] - xdata[0] 696 | if xdelta_ > xdelta: 697 | xdelta = xdelta_ 698 | loc = match.group(1) 699 | elif xdelta_ == xdelta: # top and bottom frame equally large 700 | # pick the largest meridian gradient in the middle of the frame 701 | xmean = xdata.mean() 702 | ylim = self.ax.get_ylim() 703 | m_bottom, p_ = self.proj.inv(xmean, ylim[0]) 704 | grad_bottom = np.abs(self.proj.gradient(m_bottom, p_, direction="parallel")[0]) 705 | m_top, p_ = self.proj.inv(xmean, ylim[1]) 706 | grad_top = np.abs(self.proj.gradient(m_top, p_, direction="parallel")[0]) 707 | if grad_top > grad_bottom: 708 | loc = 'top' 709 | else: 710 | loc = 'bottom' 711 | if loc not in frame_locs: 712 | return 713 | 714 | size = kwargs.pop('size', matplotlib.rcParams['font.size']) 715 | # styling consistent with frame, i.e. with edge 716 | color = kwargs.pop('color', self._edge.get_edgecolor()) 717 | alpha = kwargs.pop('alpha', self._edge.get_alpha()) 718 | zorder = kwargs.pop('zorder', self._edge.get_zorder() + 1) # on top of edge 719 | horizontalalignment = kwargs.pop('horizontalalignment', 'center') 720 | verticalalignment = self._negateLoc(loc) # no option along the frame 721 | _ = kwargs.pop('verticalalignment', None) 722 | 723 | if pad is None: 724 | pad = size / 3 725 | pad /= 72 726 | 727 | if meridians is None: 728 | meridians = self.meridians 729 | 730 | # find all meridian grid lines 731 | m_artists = self.artists(r'grid-meridian-([\-\+0-9.]+)', regex=True) 732 | pos = locs[loc] 733 | xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() 734 | xticks = [] 735 | xticklabels = [] 736 | for c,match in m_artists: 737 | m = float(match.group(1)) 738 | if m in meridians: 739 | # intersect with axis 740 | xm, ym = c.get_xdata(), c.get_ydata() 741 | xm_at_ylim = extrap(ylim, ym, xm)[pos] 742 | if xm_at_ylim >= xlim[0] and xm_at_ylim <= xlim[1] and self.contains(xm_at_ylim, ylim[pos]): 743 | # compute the offset and the location of the label by following the meridian tangent 744 | m_, p_ = self.proj.inv(xm_at_ylim, ylim[pos]) 745 | dxy = self.proj.gradient(m_, p_, direction="meridian") 746 | dxy *= pad / np.sqrt((dxy**2).sum()) 747 | if loc == "bottom": 748 | dxy *= -1 749 | dxy[1] = 0 # only offset in x 750 | 751 | # move the position of the label by dx: 752 | # go from data coordinates to display, offset in display, go back to data 753 | offset = matplotlib.transforms.ScaledTranslation(*dxy, self.fig.dpi_scale_trans) 754 | data_offset = self.ax.transData + offset + self.ax.transData.inverted() 755 | xy_at_ylim = data_offset.transform((xm_at_ylim, ylim[pos])) 756 | xticks.append(xy_at_ylim[0]) 757 | xticklabels.append(self._config['grid']['meridian_fmt'](m)) 758 | 759 | if len(xticks): 760 | self.ax.xaxis.set_visible(True) 761 | self.ax.xaxis.set_ticks_position(loc) 762 | self.ax.xaxis.set_label_position(loc) 763 | self.ax.xaxis.set_ticks(xticks) 764 | self.ax.xaxis.set_ticklabels(xticklabels) 765 | 766 | 767 | def labelParallelsAtFrame(self, loc='auto', parallels=None, pad=None, **kwargs): 768 | """Label the parallels on rectangular frame of the map 769 | 770 | If the view only shows a fraction of the map, a segment or an entire 771 | rectangular frame is shown and the graticule labels are moved to outside 772 | that frame. This method is implicitly called, but can be used to overwrite 773 | the defaults. 774 | 775 | Args: 776 | loc: location of the label with respect to frame, from `['left', 'right']` 777 | parallels: list of parallels to label, if None labels all of them 778 | pad: padding of annotation, in units of fontsize 779 | **kwargs: styling of `matplotlib` annotations for the graticule labels 780 | """ 781 | assert loc in [None, 'none', 'auto', 'left', 'right'] 782 | 783 | arguments = _parseArgs(locals()) 784 | myname = 'labelParallelsAtFrame' 785 | self._config[myname] = arguments 786 | 787 | # remove existing 788 | self.ax.yaxis.set_visible(False) 789 | frame_artists = self.artists('frame-parallel-label') 790 | for artist in frame_artists: 791 | artist.remove() 792 | 793 | # check if loc has frame, check both frames for auto 794 | locs = {"left": 0, "right": 1} 795 | frame_artists = self.artists(r'frame-([a-zA-Z]+)', regex=True) 796 | frame_locs = [match.group(1) for c,match in frame_artists] 797 | if loc == "auto": 798 | loc = None 799 | ydelta = 0 800 | for c,match in frame_artists: 801 | if match.group(1) in locs.keys(): 802 | ydata = c.get_ydata() 803 | ydelta_ = ydata[-1] - ydata[0] 804 | if ydelta_ > ydelta: 805 | ydelta = ydelta_ 806 | loc = match.group(1) 807 | elif ydelta_ == ydelta: # top and bottom frame equally large 808 | # pick the largest meridian gradient in the middle of the frame 809 | ymean = ydata.mean() 810 | xlim = self.ax.get_xlim() 811 | m_, p_left = self.proj.inv(xlim[0], ymean) 812 | grad_left = np.abs(self.proj.gradient(m_, p_left, direction="meridian")[1]) 813 | m_, p_right = self.proj.inv(xlim[1], ymean) 814 | grad_right = np.abs(self.proj.gradient(m_, p_right, direction="meridian")[1]) 815 | if grad_right > grad_left: 816 | loc = 'right' 817 | else: 818 | loc = 'left' 819 | 820 | if loc not in frame_locs: 821 | return 822 | 823 | size = kwargs.pop('size', matplotlib.rcParams['font.size']) 824 | # styling consistent with frame, i.e. with edge 825 | color = kwargs.pop('color', self._edge.get_edgecolor()) 826 | alpha = kwargs.pop('alpha', self._edge.get_alpha()) 827 | zorder = kwargs.pop('zorder', self._edge.get_zorder() + 1) # on top of edge 828 | verticalalignment = kwargs.pop('verticalalignment', 'center') 829 | horizontalalignment = self._negateLoc(loc) # no option along the frame 830 | _ = kwargs.pop('horizontalalignment', None) 831 | 832 | if pad is None: 833 | pad = size / 3 834 | pad /= 72 # in inch 835 | 836 | if parallels is None: 837 | parallels = self.parallels 838 | 839 | # find all parallel grid lines 840 | p_artists = self.artists(r'grid-parallel-([\-\+0-9.]+)', regex=True) 841 | pos = locs[loc] 842 | xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() 843 | yticks = [] 844 | yticklabels = [] 845 | for c,match in p_artists: 846 | p = float(match.group(1)) 847 | if p in parallels: 848 | # intersect with axis 849 | xp, yp = c.get_xdata(), c.get_ydata() 850 | yp_at_xlim = extrap(xlim, xp, yp)[pos] 851 | if yp_at_xlim >= ylim[0] and yp_at_xlim <= ylim[1] and self.contains(xlim[pos], yp_at_xlim): 852 | m_, p_ = self.proj.inv(xlim[pos], yp_at_xlim) 853 | dxy = self.proj.gradient(m_, p_, direction='parallel') 854 | dxy *= pad / np.sqrt((dxy**2).sum()) 855 | if (self.proj.lon_type == "ra" and loc == "right") or (self.proj.lon_type != "ra" and loc == "left"): 856 | dxy *= -1 857 | dxy[0] = 0 # only offset in y 858 | 859 | # move the position of the label by dy: 860 | # go from data coordinates to display, offset in display, go back to data 861 | offset = matplotlib.transforms.ScaledTranslation(*dxy, self.fig.dpi_scale_trans) 862 | data_offset = self.ax.transData + offset + self.ax.transData.inverted() 863 | xy_at_xlim = data_offset.transform((xlim[pos], yp_at_xlim)) 864 | yticks.append(xy_at_xlim[1]) 865 | yticklabels.append(self._config['grid']['parallel_fmt'](p)) 866 | 867 | if len(yticks): 868 | self.ax.yaxis.set_visible(True) 869 | self.ax.yaxis.set_ticks_position(loc) 870 | self.ax.yaxis.set_label_position(loc) 871 | self.ax.yaxis.set_ticks(yticks) 872 | self.ax.yaxis.set_ticklabels(yticklabels) 873 | 874 | 875 | def _setFrame(self): 876 | # clean up existing frame 877 | frame_artists = self.artists(r'frame-([a-zA-Z]+)', regex=True) 878 | for c,m in frame_artists: 879 | c.remove() 880 | 881 | locs = ['left', 'bottom', 'right', 'top'] 882 | xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() 883 | 884 | # use styling of edge for consistent map borders 885 | ls = '-' 886 | lw = self._edge.get_linewidth() 887 | c = self._edge.get_edgecolor() 888 | alpha = self._edge.get_alpha() 889 | zorder = self._edge.get_zorder() - 1 # limits imprecise, hide underneath edge 890 | 891 | precision = 1000 892 | const = np.ones(precision) 893 | for loc in locs: 894 | # define line along axis 895 | if loc == "left": 896 | line = xlim[0]*const, np.linspace(ylim[0], ylim[1], precision) 897 | if loc == "right": 898 | line = xlim[1]*const, np.linspace(ylim[0], ylim[1], precision) 899 | if loc == "bottom": 900 | line = np.linspace(xlim[0], xlim[1], precision), ylim[0]*const 901 | if loc == "top": 902 | line = np.linspace(xlim[0], xlim[1], precision), ylim[1]*const 903 | 904 | # show axis lines only where line is inside of map edge 905 | inside = self.contains(*line) 906 | if (~inside).all(): 907 | continue 908 | 909 | if inside.all(): 910 | startpos, stoppos = 0, -1 911 | xmin = (line[0][startpos] - xlim[0])/(xlim[1]-xlim[0]) 912 | ymin = (line[1][startpos] - ylim[0])/(ylim[1]-ylim[0]) 913 | xmax = (line[0][stoppos] - xlim[0])/(xlim[1]-xlim[0]) 914 | ymax = (line[1][stoppos] - ylim[0])/(ylim[1]-ylim[0]) 915 | self.ax.plot([xmin,xmax], [ymin, ymax], c=c, ls=ls, lw=lw, alpha=alpha, zorder=zorder, clip_on=False, transform=self.ax.transAxes, gid='frame-%s' % loc) 916 | continue 917 | 918 | # for piecewise inside: determine limits where it's inside 919 | # by checking for jumps in inside 920 | inside = inside.astype("int") 921 | diff = inside[1:] - inside[:-1] 922 | jump = np.flatnonzero(diff) 923 | start = 0 924 | if inside[0]: 925 | jump = np.concatenate(((0,),jump)) 926 | 927 | while True: 928 | startpos = jump[start] 929 | if start+1 < len(jump): 930 | stoppos = jump[start + 1] 931 | else: 932 | stoppos = -1 933 | 934 | xmin = (line[0][startpos] - xlim[0])/(xlim[1]-xlim[0]) 935 | ymin = (line[1][startpos] - ylim[0])/(ylim[1]-ylim[0]) 936 | xmax = (line[0][stoppos] - xlim[0])/(xlim[1]-xlim[0]) 937 | ymax = (line[1][stoppos] - ylim[0])/(ylim[1]-ylim[0]) 938 | artist = Line2D([xmin,xmax], [ymin, ymax], c=c, ls=ls, lw=lw, alpha=alpha, zorder=zorder, clip_on=False, transform=self.ax.transAxes, gid='frame-%s' % loc) 939 | self.ax.add_line(artist) 940 | if start + 2 < len(jump): 941 | start += 2 942 | else: 943 | break 944 | 945 | def _clearFrame(self): 946 | frame_artists = self.artists('frame-') 947 | for artist in frame_artists: 948 | artist.remove() 949 | 950 | def _resetFrame(self): 951 | self._setFrame() 952 | for method in ['labelMeridiansAtFrame', 'labelParallelsAtFrame']: 953 | if method in self._config.keys(): 954 | getattr(self, method)(**self._config[method]) 955 | 956 | def _pressHandler(self, evt): 957 | if evt.button != 1: return 958 | if evt.dblclick: return 959 | # remove frame and labels 960 | self._clearFrame() 961 | self.fig.canvas.draw() 962 | 963 | def _releaseHandler(self, evt): 964 | if evt.button != 1: return 965 | if evt.dblclick: return 966 | self._resetFrame() 967 | self.fig.canvas.draw() 968 | 969 | def _scrollHandler(self, evt): 970 | # mouse scroll for zoom 971 | if evt.inaxes != self.ax: return 972 | if evt.step == 0: return 973 | 974 | # remove frame and labels 975 | self._clearFrame() 976 | 977 | # scroll to fixed pointer position: google maps style 978 | factor = 0.1 979 | c = 1 - evt.step*factor # scaling factor 980 | xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() 981 | xdiff, ydiff = xlim[1] - xlim[0], ylim[1] - ylim[0] 982 | x, y = evt.xdata, evt.ydata 983 | fx, fy = (x - xlim[0])/xdiff, (y - ylim[0])/ydiff # axis units 984 | xlim_, ylim_ = x - fx*c*xdiff, y - fy*c*ydiff 985 | xlim__, ylim__ = xlim_ + c*xdiff, ylim_ + c*ydiff 986 | 987 | self.ax.set_xlim(xlim_, xlim__) 988 | self.ax.set_ylim(ylim_, ylim__) 989 | self._resetFrame() 990 | self.fig.canvas.draw() 991 | 992 | #### common plot type for maps: follow mpl convention #### 993 | def plot(self, lon, lat, *args, **kwargs): 994 | """Matplotlib `plot` with `lon/lat` points transformed according to map projection""" 995 | x, y = self.proj.transform(lon, lat) 996 | return self.ax.plot(x, y, *args, **kwargs) 997 | 998 | def scatter(self, lon, lat, **kwargs): 999 | """Matplotlib `scatter` with `lon/lat` points transformed according to map projection""" 1000 | x, y = self.proj.transform(lon, lat) 1001 | return self.ax.scatter(x, y, **kwargs) 1002 | 1003 | def hexbin(self, lon, lat, C=None, **kwargs): 1004 | """Matplotlib `hexbin` with `lon/lat` points transformed according to map projection""" 1005 | x, y = self.proj.transform(lon, lat) 1006 | # determine proper gridsize: by default x is only needed, y is chosen accordingly 1007 | gridsize = kwargs.pop("gridsize", None) 1008 | mincnt = kwargs.pop("mincnt", 1) 1009 | clip_path = kwargs.pop('clip_path', self._edge) 1010 | if gridsize is None: 1011 | xlim, ylim = (x.min(), x.max()), (y.min(), y.max()) 1012 | per_sample_volume = (xlim[1]-xlim[0])**2 / x.size * 10 1013 | gridsize = (int(np.round((xlim[1]-xlim[0]) / np.sqrt(per_sample_volume))), 1014 | int(np.round((ylim[1]-ylim[0]) / np.sqrt(per_sample_volume)) / np.sqrt(3))) 1015 | 1016 | # styling: use same default colormap as density for histogram 1017 | if C is None: 1018 | cmap = kwargs.pop("cmap", "YlOrRd") 1019 | else: 1020 | cmap = kwargs.pop("cmap", None) 1021 | zorder = kwargs.pop("zorder", 0) # same as for imshow: underneath everything 1022 | 1023 | artist = self.ax.hexbin(x, y, C=C, gridsize=gridsize, mincnt=mincnt, cmap=cmap, zorder=zorder, **kwargs) 1024 | artist.set_clip_path(clip_path) 1025 | return artist 1026 | 1027 | def text(self, lon, lat, s, rotation=None, direction="parallel", **kwargs): 1028 | """Matplotlib `text` with coordinates given by `lon/lat`. 1029 | 1030 | Args: 1031 | lon: longitude of text 1032 | lat: latitude of text 1033 | s: string 1034 | rotation: if text should be rotated to tangent direction 1035 | direction: tangent direction, from `['parallel', 'meridian']` 1036 | **kwargs: styling arguments for `matplotlib.text` 1037 | """ 1038 | x, y = self.proj(lon, lat) 1039 | 1040 | if rotation is None: 1041 | dxy_ = self.proj.gradient(lon, lat, direction=direction) 1042 | if self.proj.lon_type == "ra": 1043 | dxy_[0] *= -1 1044 | angle = 90-np.arctan2(*dxy_) / DEG2RAD 1045 | else: 1046 | angle = rotation 1047 | 1048 | return self.ax.text(x, y, s, rotation=angle, rotation_mode="anchor", clip_on=True, **kwargs) 1049 | 1050 | def colorbar(self, cb_collection, cb_label="", loc="right", size="2%", pad="1%"): 1051 | """Add colorbar to side of map. 1052 | 1053 | The location of the colorbar will be chosen automatically to not interfere 1054 | with the map frame labels. 1055 | 1056 | Args: 1057 | cb_collection: a `matplotlib` mappable collection 1058 | cb_label: string for colorbar label 1059 | orientation: from ["vertical", "horizontal"] 1060 | size: fraction of ax size to use for colorbar 1061 | pad: fraction of ax size to use as pad to map frame 1062 | """ 1063 | assert loc in ["top", "bottom", "left", "right"] 1064 | 1065 | # move frame ticks to other side: colorbar side is taken 1066 | if loc in["top", "bottom"]: 1067 | orientation = "horizontal" 1068 | params = self._config['labelMeridiansAtFrame'] 1069 | params['loc'] = self._negateLoc(loc) 1070 | self.labelMeridiansAtFrame(**params) 1071 | if loc in["left", "right"]: 1072 | orientation = "vertical" 1073 | params = self._config['labelParallelsAtFrame'] 1074 | params['loc'] = self._negateLoc(loc) 1075 | self.labelParallelsAtFrame(**params) 1076 | 1077 | from mpl_toolkits.axes_grid1 import make_axes_locatable 1078 | divider = make_axes_locatable(self.ax) 1079 | cax = divider.append_axes(loc, size=size, pad=pad) 1080 | cb = self.fig.colorbar(cb_collection, cax=cax, orientation=orientation, ticklocation=loc) 1081 | cb.solids.set_edgecolor("face") 1082 | cb.set_label(cb_label) 1083 | return cb 1084 | 1085 | def title(self, label, **kwargs): 1086 | return self.ax.set_title(label, **kwargs) 1087 | 1088 | def focus(self, lon, lat, pad=0.025): 1089 | """Focus onto region of map covered by `lon/lat` 1090 | 1091 | Adjusts x/y limits to encompass given `lon/lat`. 1092 | 1093 | Args: 1094 | lon: list of longitudes 1095 | lat: list of latitudes 1096 | pad: distance to edge of the frame, in units of axis size 1097 | """ 1098 | # to replace the autoscale function that cannot zoom in 1099 | x, y = self.proj.transform(lon, lat) 1100 | xlim = [x.min(), x.max()] 1101 | ylim = [y.min(), y.max()] 1102 | xrange = xlim[1]-xlim[0] 1103 | yrange = ylim[1]-ylim[0] 1104 | xlim[0] -= pad * xrange 1105 | xlim[1] += pad * xrange 1106 | ylim[0] -= pad * yrange 1107 | ylim[1] += pad * yrange 1108 | self.ax.set_xlim(xlim) 1109 | self.ax.set_ylim(ylim) 1110 | self._resetFrame() 1111 | self.fig.canvas.draw() 1112 | 1113 | def defocus(self, pad=0.025): 1114 | """Show entire map. 1115 | 1116 | Args: 1117 | pad: distance to edge of the map, in units of axis size 1118 | """ 1119 | # to replace the autoscale function that cannot zoom in 1120 | xlim, ylim = list(self.xlim()), list(self.ylim()) 1121 | xrange = xlim[1]-xlim[0] 1122 | yrange = ylim[1]-ylim[0] 1123 | xlim[0] -= pad * xrange 1124 | xlim[1] += pad * xrange 1125 | ylim[0] -= pad * yrange 1126 | ylim[1] += pad * yrange 1127 | self.ax.set_xlim(xlim) 1128 | self.ax.set_ylim(ylim) 1129 | self._clearFrame() 1130 | self.fig.canvas.draw() 1131 | 1132 | def show(self, *args, **kwargs): 1133 | """Show `matplotlib` figure""" 1134 | self.fig.show(*args, **kwargs) 1135 | 1136 | def savefig(self, *args, **kwargs): 1137 | """Save `matplotlib` figure""" 1138 | self.fig.savefig(*args, **kwargs) 1139 | 1140 | #### special plot types for maps #### 1141 | 1142 | def vertex(self, vertices, color=None, vmin=None, vmax=None, **kwargs): 1143 | """Plot polygons (e.g. Healpix vertices) 1144 | 1145 | Args: 1146 | vertices: cell boundaries in RA/Dec, from getCountAtLocations() 1147 | color: string or matplib color, or numeric array to set polygon colors 1148 | vmin: if color is numeric array, use vmin to set color of minimum 1149 | vmax: if color is numeric array, use vmin to set color of minimum 1150 | **kwargs: matplotlib.collections.PolyCollection keywords 1151 | Returns: 1152 | matplotlib.collections.PolyCollection 1153 | """ 1154 | vertices_ = np.empty_like(vertices) 1155 | vertices_[:,:,0], vertices_[:,:,1] = self.proj.transform(vertices[:,:,0], vertices[:,:,1]) 1156 | 1157 | # remove vertices which are split at the outer meridians 1158 | # find variance of vertice nodes large compared to dispersion of centers 1159 | centers = np.mean(vertices, axis=1) 1160 | x, y = self.proj.transform(centers[:,0], centers[:,1]) 1161 | var = np.sum(np.var(vertices_, axis=1), axis=-1) / (x.var() + y.var()) 1162 | sel = var < 0.05 1163 | vertices_ = vertices_[sel] 1164 | 1165 | from matplotlib.collections import PolyCollection 1166 | zorder = kwargs.pop("zorder", 0) # same as for imshow: underneath everything 1167 | rasterized = kwargs.pop('rasterized', True) 1168 | alpha = kwargs.pop('alpha', 1) 1169 | if alpha < 1: 1170 | lw = kwargs.pop('lw', 0) 1171 | else: 1172 | lw = kwargs.pop('lw', None) 1173 | coll = PolyCollection(vertices_, zorder=zorder, rasterized=rasterized, alpha=alpha, lw=lw, **kwargs) 1174 | if color is not None: 1175 | coll.set_array(color[sel]) 1176 | coll.set_clim(vmin=vmin, vmax=vmax) 1177 | coll.set_edgecolor("face") 1178 | self.ax.add_collection(coll) 1179 | self.ax.set_rasterization_zorder(zorder) 1180 | return coll 1181 | 1182 | def healpix(self, m, nest=False, color_percentiles=[10,90], **kwargs): 1183 | """Plot HealPix map 1184 | 1185 | Args: 1186 | m: Healpix map array 1187 | nest: HealPix NEST scheme 1188 | color_percentiles: lower and higher cutoff percentile for map coloring 1189 | **kwargs: matplotlib.imshow keywords 1190 | """ 1191 | 1192 | # determine lon, lat for all pixels visible in the map 1193 | clip_path = kwargs.pop('clip_path', self._edge) 1194 | if clip_path is None: 1195 | xlim, ylim = self.xlim(), self.ylim() 1196 | else: 1197 | xlim = clip_path.xy[:, 0].min(), clip_path.xy[:, 0].max() 1198 | ylim = clip_path.xy[:, 1].min(), clip_path.xy[:, 1].max() 1199 | # determine number of pixel in xy limits 1200 | x_pixel_min, y_pixel_min = self.ax.transData.transform((xlim[0], ylim[0])) 1201 | x_pixel_max, y_pixel_max = self.ax.transData.transform((xlim[1], ylim[1])) 1202 | width, height = int(math.ceil(x_pixel_max - x_pixel_min)), int(math.ceil(y_pixel_max - y_pixel_min)) 1203 | # make a grid in xy coordinates with the determined number of pixels 1204 | xline = np.linspace(xlim[0], xlim[1], width) 1205 | yline = np.linspace(ylim[0], ylim[1], height) 1206 | xp, yp = np.meshgrid(xline, yline) 1207 | # compute coordinate for the pixels that are inside map 1208 | inside = self.contains(xp, yp) 1209 | xp = np.ma.array(xp, mask=~inside) 1210 | yp = np.ma.array(yp, mask=~inside) 1211 | lonp, latp = self.proj.inv(xp, yp) 1212 | 1213 | # determine healpix ipix for all visible and valid map pixels 1214 | valid = np.isfinite(lonp.data) & np.isfinite(latp.data) 1215 | inside &= valid 1216 | nside = healpix.hp.npix2nside(m.size) 1217 | ipix = healpix.hp.ang2pix(nside, lonp[inside], latp[inside], lonlat=True, nest=nest) # use lonlat=True! 1218 | 1219 | # copy values from m into vp 1220 | # but check if m is a masked array to avoid plotting masked values 1221 | vp = np.ma.array(np.empty(xp.shape), mask=~inside) 1222 | if np.__version__ >= "1.1": 1223 | matype = np.ma.core.MaskedArray 1224 | else: 1225 | matype = np.ma.array 1226 | if not isinstance(m, matype): 1227 | vp.data[inside] = m[ipix] 1228 | else: 1229 | vp.data[inside] = m.data[ipix] 1230 | vp.mask[inside] |= m.mask[ipix] 1231 | 1232 | # color range 1233 | vmin = kwargs.pop("vmin", None) 1234 | vmax = kwargs.pop("vmax", None) 1235 | if vmin is None or vmax is None: 1236 | vlim = np.percentile(vp.data[inside], color_percentiles) 1237 | # if only a few different values exist, percentiles don't work: 1238 | # default to min/max 1239 | if vlim[0] == vlim[1]: 1240 | vlim = (vp.data[inside].min(), vp.data[inside].max()) 1241 | if vmin is None: 1242 | vmin = vlim[0] 1243 | if vmax is None: 1244 | vmax = vlim[1] 1245 | 1246 | # show the vp image 1247 | cmap = kwargs.pop("cmap", "YlOrRd") 1248 | zorder = kwargs.pop("zorder", 0) # same as for imshow: underneath everything 1249 | kwargs.pop("origin", None) # remove origin and force ... 1250 | origin = "lower" 1251 | xlim_, ylim_ = self.ax.get_xlim(), self.ax.get_ylim() 1252 | artist = self.ax.imshow( 1253 | vp, 1254 | extent=(xlim[0], xlim[1], ylim[0], ylim[1]), 1255 | vmin=vmin, 1256 | vmax=vmax, 1257 | cmap=cmap, 1258 | zorder=zorder, 1259 | origin=origin, 1260 | **kwargs 1261 | ) 1262 | artist.set_clip_path(clip_path) 1263 | # ... because imshow focusses on extent 1264 | self.ax.set_xlim(xlim_) 1265 | self.ax.set_ylim(ylim_) 1266 | return artist 1267 | 1268 | def footprint(self, survey, nside, **kwargs): 1269 | """Plot survey footprint onto map 1270 | 1271 | Uses `contains()` method of a `skymapper.Survey` derived class instance 1272 | 1273 | Args: 1274 | survey: name of the survey, must be in keys of `skymapper.survey_register` 1275 | nside: HealPix nside 1276 | **kwargs: styling of `matplotlib.collections.PolyCollection` 1277 | """ 1278 | 1279 | pixels, rap, decp, vertices = healpix.getGrid(nside, return_vertices=True) 1280 | inside = survey.contains(rap, decp) 1281 | return self.vertex(vertices[inside], **kwargs) 1282 | 1283 | def density(self, lon, lat, nside=1024, mask_empty=True, **kwargs): 1284 | """Plot sample density using healpix binning 1285 | 1286 | Args: 1287 | lon: list of longitudes 1288 | lat: list of latitudes 1289 | nside: HealPix nside 1290 | mask_empty: Whether empty cells should be hidden. 1291 | **kwargs: additional arguments for healpix() 1292 | """ 1293 | # get count in healpix cells, restrict to non-empty cells 1294 | bc = healpix.getCountAtLocations(lon, lat, nside=nside) 1295 | if mask_empty: 1296 | bc = np.ma.array(bc, mask=(bc==0)) 1297 | 1298 | # styling 1299 | vmin = kwargs.pop("vmin", 0) # densities cannot be negative 1300 | cmap = kwargs.pop("cmap", "YlOrRd") 1301 | 1302 | # make map 1303 | return self.healpix(bc, vmin=vmin, cmap=cmap, **kwargs) 1304 | 1305 | def extrapolate(self, lon, lat, value, resolution=100, **kwargs): 1306 | """Extrapolate lon,lat,value samples on the entire sphere 1307 | 1308 | Requires scipy, uses `scipy.interpolate.Rbf`. 1309 | 1310 | Args: 1311 | lon: list of longitudes 1312 | lat: list of latitudes 1313 | value: list of sample values 1314 | resolution: number of evaluated cells per linear map dimension 1315 | **kwargs: arguments for matplotlib.imshow 1316 | """ 1317 | # interpolate samples in lon/lat 1318 | # this should be evaluated with angular distances, but that's very slow and has unstable extrapolation 1319 | # we use Euclidean norm instead 1320 | rbfi = scipy.interpolate.Rbf(lon, lat, value) # , norm=angularDistance) 1321 | 1322 | # make grid in x/y over the limits of the map or the clip_path 1323 | clip_path = kwargs.pop('clip_path', self._edge) 1324 | if clip_path is None: 1325 | xlim, ylim = self.xlim(), self.ylim() 1326 | else: 1327 | xlim = clip_path.xy[:, 0].min(), clip_path.xy[:, 0].max() 1328 | ylim = clip_path.xy[:, 1].min(), clip_path.xy[:, 1].max() 1329 | 1330 | if resolution % 1 == 0: 1331 | resolution += 1 1332 | 1333 | dx = (xlim[1]-xlim[0])/resolution 1334 | xline = np.arange(xlim[0], xlim[1], dx) 1335 | yline = np.arange(ylim[0], ylim[1], dx) 1336 | xp, yp = np.meshgrid(xline, yline) + dx/2 # evaluate center pixel 1337 | inside = self.contains(xp,yp) 1338 | xp = np.ma.array(xp, mask=~inside) 1339 | yp = np.ma.array(yp, mask=~inside) 1340 | lonp, latp = self.proj.inv(xp, yp) 1341 | 1342 | # evaluate RBF for all visible and valid map pixels 1343 | valid = np.isfinite(lonp.data) & np.isfinite(latp.data) 1344 | inside &= valid 1345 | vp = np.ma.array(np.empty(xp.shape), mask=~inside) 1346 | vp[inside] = rbfi(lonp[inside], latp[inside]) 1347 | 1348 | # xp,yp whose centers aren't in the map have no values: 1349 | # edges of map are not clean 1350 | # construct another rbf in pixel space to populate the values 1351 | # outside of the map region, then use clip-path to clip them at the edges 1352 | # ordinary Euclidean distance now 1353 | rbfi = scipy.interpolate.Rbf(xp[inside], yp[inside], vp[inside]) 1354 | vp[~inside] = rbfi(xp[~inside], yp[~inside]) 1355 | 1356 | zorder = kwargs.pop("zorder", 0) # same as for imshow: underneath everything 1357 | kwargs.pop("origin", None) # remove origin and force ... 1358 | origin = "lower" 1359 | xlim_, ylim_ = self.ax.get_xlim(), self.ax.get_ylim() 1360 | artist = self.ax.imshow(vp, extent=(xlim[0], xlim[1], ylim[0], ylim[1]), zorder=zorder, origin=origin, **kwargs) 1361 | artist.set_clip_path(clip_path) 1362 | # ... because imshow focusses on extent 1363 | self.ax.set_xlim(xlim_) 1364 | self.ax.set_ylim(ylim_) 1365 | return artist 1366 | -------------------------------------------------------------------------------- /skymapper/projection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.integrate 3 | import scipy.optimize 4 | 5 | DEG2RAD = np.pi/180 6 | 7 | def _toArray(x): 8 | """Convert x to array if needed 9 | 10 | Returns: 11 | array(x), boolean if x was an array before 12 | """ 13 | if hasattr(x, '__iter__'): 14 | return np.array(x), True 15 | return np.array([x], dtype=np.double), False 16 | 17 | def ellipticity(a, b): 18 | """Returns 1-abs(b/a)""" 19 | return 1-np.abs(b/a) 20 | def meanDistortion(a, b): 21 | """Returns average `ellipticity` over all `a`,`b`""" 22 | return np.mean(ellipticity(a,b)) 23 | def maxDistortion(a,b): 24 | """Returns max `ellipticity` over all `a`,`b`""" 25 | return np.max(ellipticity(a,b)) 26 | def stdDistortion(a,b): 27 | """Returns `std(b/a)`""" 28 | return (b/a).std() # include the sign 29 | def stdScale(a,b): 30 | """Returns `std(a*b)` 31 | 32 | This is useful for conformal projections. 33 | """ 34 | return (a*b).std() 35 | def stdDistortionScale(a,b): 36 | """Retruns sum of `stdScale` and `stdDistortion`. 37 | 38 | This is useful for a compromise between equal-area and conformal projections. 39 | """ 40 | return stdScale(a,b) + stdDistortion(a,b) 41 | 42 | def _optimize_objective(x, proj_cls, lon_type, lon, lat, crit): 43 | """Construct projections from parameters `x` and compute `crit` for `lon, lat`""" 44 | proj = proj_cls(*x, lon_type=lon_type) 45 | a, b = proj.distortion(lon, lat) 46 | return crit(a,b) 47 | 48 | def _optimize(proj_cls, x0, lon_type, lon, lat, crit, bounds=None): 49 | """Determine parameters for `proj_cls` that minimize `crit` over `lon, lat`. 50 | 51 | Args: 52 | proj_cls: projection class 53 | x0: arguments for projection class `__init__` 54 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 55 | lon: list of rectascensions 56 | lat: list of declinations 57 | crit: optimization criterion 58 | needs to be function of semi-major and semi-minor axes of the Tissot indicatix 59 | bounds: list of upper and lower bounds on each parameter in `x0` 60 | 61 | Returns: 62 | optimized projection of class `proj_cls` 63 | """ 64 | print ("optimizing parameters of %s to minimize %s" % (proj_cls.__name__, crit.__name__)) 65 | x, fmin, d = scipy.optimize.fmin_l_bfgs_b(_optimize_objective, x0, args=(proj_cls, lon_type, lon, lat, crit), bounds=bounds, approx_grad=True) 66 | res = proj_cls(*x, lon_type=lon_type) 67 | print ("best objective %.6f at %r" % (fmin, res)) 68 | return res 69 | 70 | def _dist(radec, proj, xy): 71 | return np.sum((xy - np.array(proj(radec[0], radec[1])))**2) 72 | 73 | 74 | class BaseProjection(object): 75 | """Projection base class 76 | 77 | Every projection needs to implement three methods: 78 | * `transform(self, lon, lat)`: mapping from lon/lat to map x/y 79 | * `invert(self, x, y)`: the inverse mapping from x/y to lon/lat 80 | 81 | All methods accept either single number or arrays and return accordingly. 82 | """ 83 | def __init__(self, lon_0=0, lon_type="ra"): 84 | """Initialize projection 85 | 86 | Args: 87 | lon_0 (int, float): reference longitude 88 | lon_type (string): type of longitude 89 | "lon" for a standard coordinate system (right-handed, -180..180 deg) 90 | "ra" for n equatorial coordinate system (left-handed, 0..360 deg) 91 | """ 92 | assert lon_type in ['ra', 'lon'] 93 | self.lon_0 = lon_0 94 | self.lon_type = lon_type 95 | if self.lon_type == "ra" and self.lon_0 < 0: 96 | self.lon_0 += 360 97 | elif self.lon_type == "lon" and self.lon_0 > 180: 98 | self.lon_0 -= 360 99 | 100 | def __call__(self, lon, lat): 101 | """Shorthand for `transform`. Works also with single coordinates 102 | 103 | Args: 104 | lon (float, array): longitude 105 | lat (float, array): latitude 106 | """ 107 | lon_, isArray = _toArray(lon) 108 | lat_, isArray = _toArray(lat) 109 | assert len(lon_) == len(lat_) 110 | 111 | x, y = self.transform(lon_, lat_) 112 | if isArray: 113 | return x, y 114 | else: 115 | return x[0], y[0] 116 | 117 | def transform(self, lon, lat): 118 | """Convert longitude/latitude into map coordinates 119 | 120 | Note: Unlike `__call__`, lon/lat need to be arrays! 121 | 122 | Args: 123 | lon (array): longitudes 124 | lat (array): latitudes 125 | 126 | Returns: 127 | x,y with the same format as lon,lat 128 | """ 129 | pass 130 | 131 | def inv(self, x, y): 132 | """Shorthand for `invert`. Works also with single coordinates 133 | 134 | Args: 135 | x (float, array): horizontal map coordinate 136 | y (float, array): vertical map coordinate 137 | """ 138 | x_, isArray = _toArray(x) 139 | y_, isArray = _toArray(y) 140 | lon, lat = self.invert(x_, y_) 141 | if isArray: 142 | return lon, lat 143 | else: 144 | return lon[0], lat[0] 145 | 146 | def invert(self, x, y): 147 | """Convert map coordinates into longitude/latitude 148 | 149 | Args: 150 | x (array): horizontal map coordinates 151 | y (array): vertical map coordinates 152 | 153 | Returns: 154 | lon,lat with the same format as x,y 155 | """ 156 | # default implementation for non-analytic inverses 157 | assert len(x) == len(y) 158 | 159 | bounds = ((None,None), (-90, 90)) # lon/lat limits 160 | start = (self.lon_0,0) # lon/lat of initial guess: should be close to map center 161 | lon, lat = np.empty(len(x)), np.empty(len(y)) 162 | i = 0 163 | for x_,y_ in zip(x, y): 164 | xy = np.array([x_,y_]) 165 | radec, fmin, d = scipy.optimize.fmin_l_bfgs_b(_dist, start, args=(self, xy), bounds=bounds, approx_grad=True) 166 | if fmin < 1e-6: # smaller than default tolerance of fmin 167 | lon[i], lat[i] = radec 168 | else: 169 | lon[i], lat[i] = -1000, -1000 170 | i += 1 171 | 172 | return lon, lat 173 | 174 | @property 175 | def poleIsPoint(self): 176 | """Whether the pole is mapped onto a point""" 177 | try: 178 | return self._poleIsPoint 179 | except AttributeError: 180 | self._poleIsPoint = {} 181 | N = 10 182 | # run along the poles from the left to right outer meridian 183 | rnd_meridian = -180 + 360*np.random.rand(N) + self.lon_0 184 | for deg in [-90, 90]: 185 | line = self.transform(rnd_meridian, deg*np.ones(N)) 186 | if np.unique(line[0]).size > 1 or np.unique(line[1]).size > 1: 187 | self._poleIsPoint[deg] = False 188 | else: 189 | self._poleIsPoint[deg] = True 190 | return self._poleIsPoint 191 | 192 | def _standardize(self, lon): 193 | """Normalize longitude to -180 .. 180, with reference `lon_0` at 0""" 194 | lon_ = lon - self.lon_0 # need copy to prevent changing data 195 | if self.lon_type == "ra": 196 | lon_ *= -1 # left-handed 197 | # check that lon_ is between -180 and 180 deg 198 | lon_[lon_ < -180 ] += 360 199 | lon_[lon_ > 180 ] -= 360 200 | return lon_ 201 | 202 | def _unstandardize(self, lon): 203 | """Revert `_standardize`""" 204 | # no copy needed since all lons have been altered/transformed before 205 | if self.lon_type == "ra": 206 | lon *= -1 # left-handed 207 | lon += self.lon_0 208 | lon [lon < 0] += 360 209 | lon [lon > 360] -= 360 210 | return lon 211 | 212 | def gradient(self, lon, lat, sep=1e-2, direction='parallel'): 213 | """Compute the gradient in map coordinates at given sky position 214 | 215 | Note: Gradient along parallel is computed in positive lon direction 216 | 217 | Args: 218 | lon: (list of) longitude 219 | lat: (list of) latitude 220 | sep: distance for symmetric first-order derivatives 221 | direction: tangent direction for gradient, from `['parallel', 'meridian']` 222 | 223 | Returns: 224 | `dx`, `dy` for every item in `lon/lat` 225 | """ 226 | assert direction in ['parallel', 'meridian'] 227 | 228 | lon_, isArray = _toArray(lon) 229 | lat_, isArray = _toArray(lat) 230 | 231 | # gradients in *positive* lat and *negative* lon 232 | if direction == 'parallel': 233 | test = np.empty((2, lon_.size)) 234 | test[0] = lon_-sep/2 235 | test[1] = lon_+sep/2 236 | 237 | # check for points beyond -180 / 180 238 | mask = test[0] <= self.lon_0 - 180 239 | test[0][mask] = lon_[mask] 240 | mask = test[1] >= self.lon_0 + 180 241 | test[1][mask] = lon_[mask] 242 | 243 | x, y = self.transform(test, np.ones((2,lon_.size))*lat) 244 | else: 245 | test = np.empty((2, lat_.size)) 246 | test[0] = lat_-sep/2 247 | test[1] = lat_+sep/2 248 | 249 | # check for points beyond -90 / 90 250 | mask = test[0] <= -90 251 | test[0][mask] = lat_[mask] 252 | mask = test[1] >= 90 253 | test[1][mask] = lat_[mask] 254 | 255 | x, y = self.transform(np.ones((2,lat_.size))*lon, test) 256 | 257 | sep = test[1] - test[0] 258 | x[0] = (x[1] - x[0])/sep # dx 259 | x[1] = (y[1] - y[0])/sep # dy 260 | if isArray: 261 | return x.T 262 | return x[:,0] 263 | 264 | def jacobian(self, lon, lat, sep=1e-2): 265 | """Jacobian of mapping from lon/lat to map coordinates x/y 266 | 267 | Args: 268 | lon: (list of) longitude 269 | lat: (list of) latitude 270 | 271 | Returns: 272 | ((dx/dlon, dx/dlat), (dy/dlon, dy/dlat)) for every item in `lon/lat` 273 | """ 274 | dxy_dra= self.gradient(lon, lat, sep=sep, direction='parallel') 275 | dxy_ddec = self.gradient(lon, lat, sep=sep, direction='meridian') 276 | return np.dstack((dxy_dra, dxy_ddec)) 277 | 278 | def distortion(self, lon, lat): 279 | """Compute semi-major and semi-minor axis according to Tissot's indicatrix 280 | 281 | See Snyder (1987, section 4) 282 | 283 | Args: 284 | lon: (list of) longitude 285 | lat: (list of) latitude 286 | 287 | Returns: 288 | a, b for every item in `lon/lat` 289 | """ 290 | jac = self.jacobian(lon,lat) 291 | cos_phi = np.cos(lat * DEG2RAD) 292 | h = np.sqrt(jac[:,0,1]**2 + jac[:,1,1]**2) 293 | k = np.sqrt(jac[:,0,0]**2 + jac[:,1,0]**2) / cos_phi 294 | sin_t = (jac[:,1,1]*jac[:,0,0] - jac[:,0,1]*jac[:,1,0])/(h*k*cos_phi) 295 | a_ = np.sqrt(np.maximum(h*h + k*k + 2*h*k*sin_t, 0)) # can be very close to 0 296 | b_ = np.sqrt(np.maximum(h*h + k*k - 2*h*k*sin_t, 0)) 297 | a = (a_ + b_) / 2 298 | b = (a_ - b_) / 2 299 | s = h*k*sin_t 300 | return a, b 301 | 302 | @classmethod 303 | def optimize(cls, lon, lat, crit=meanDistortion, lon_type="ra"): 304 | """Optimize the parameters of projection to minimize `crit` over `lon,lat` 305 | 306 | Args: 307 | lon: list of longitude 308 | lat: list of latitude 309 | crit: optimization criterion 310 | needs to be function of semi-major and semi-minor axes of the Tissot indicatix 311 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 312 | 313 | Returns: 314 | optimized projection 315 | """ 316 | lon_ = np.array(lon) 317 | # go into standard frame, right or left-handed is irrelevant here 318 | lon_[lon_ > 180] -= 360 319 | lon_[lon_ < -180] += 360 320 | bounds = ((-180,180),) 321 | x0 = np.array((lon_.mean(),)) 322 | return _optimize(cls, x0, lon_type, lon, lat, crit, bounds=bounds) 323 | 324 | def __repr__(self): 325 | return "%s(%r)" % (self.__class__.__name__, self.lon_0) 326 | 327 | 328 | # metaclass for registration. 329 | # see https://effectivepython.com/2015/02/02/register-class-existence-with-metaclasses/ 330 | 331 | from . import register_projection, with_metaclass 332 | # [blatant copy from six to avoid dependency] 333 | # python 2 and 3 compatible metaclasses 334 | # see http://python-future.org/compatible_idioms.html#metaclasses 335 | 336 | class Meta(type): 337 | def __new__(meta, name, bases, class_dict): 338 | cls = type.__new__(meta, name, bases, class_dict) 339 | 340 | # remove those that are directly derived from BaseProjection 341 | if BaseProjection not in bases: 342 | register_projection(cls) 343 | 344 | return cls 345 | 346 | class Projection(with_metaclass(Meta, BaseProjection)): 347 | pass 348 | 349 | 350 | class ConicProjection(BaseProjection): 351 | def __init__(self, lon_0, lat_0, lat_1, lat_2, lon_type="ra"): 352 | """Base class for conic projections 353 | 354 | Args: 355 | lon_0: longitude that maps onto x = 0 356 | lat_0: latitude that maps onto y = 0 357 | lat_1: lower standard parallel 358 | lat_2: upper standard parallel (must not be -lat_1) 359 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 360 | """ 361 | super(ConicProjection, self).__init__(lon_0, lon_type) 362 | self.lat_0 = lat_0 363 | self.lat_1 = lat_1 364 | self.lat_2 = lat_2 365 | if lat_1 > lat_2: 366 | self.lat_1, self.lat_2 = self.lat_2, self.lat_1 367 | 368 | @classmethod 369 | def optimize(cls, lon, lat, crit=meanDistortion, lon_type="ra"): 370 | """Optimize the parameters of projection to minimize `crit` over `lon,lat` 371 | 372 | Uses median latitude and latitude-weighted longitude as reference, 373 | and places standard parallels 1/6 inwards from the min/max latitude 374 | to minimize scale variations (Snyder 1987, section 14). 375 | 376 | Args: 377 | lon: list of longitude 378 | lat: list of latitude 379 | crit: optimization criterion 380 | needs to be function of semi-major and semi-minor axes of the Tissot indicatix 381 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 382 | 383 | Returns: 384 | optimized projection 385 | """ 386 | # for conics: need to determine central lon, lat plus two standard parallels 387 | # normalize lon 388 | lon_ = np.array(lon) 389 | lon_[lon_ > 180] -= 360 390 | lon_[lon_ < -180] += 360 391 | # weigh more towards the poles because that decreases distortions 392 | lon0 = (lon_ * lat).sum() / lat.sum() 393 | if lon0 < 0: 394 | lon0 += 360 395 | lat0 = np.median(lat) 396 | 397 | # determine standard parallels 398 | lat1, lat2 = lat.min(), lat.max() 399 | delta_lat = (lat0 - lat1, lat2 - lat0) 400 | lat1 += delta_lat[0]/6 401 | lat2 -= delta_lat[1]/6 402 | 403 | x0 = np.array((lon0, lat0, lat1, lat2)) 404 | bounds = ((0, 360), (-90,90),(-90,90), (-90,90)) 405 | return _optimize(cls, x0, lon_type, lon, lat, crit, bounds=bounds) 406 | 407 | def __repr__(self): 408 | return "%s(%r,%r,%r,%r)" % (self.__class__.__name__, self.lon_0, self.lat_0, self.lat_1, self.lat_2) 409 | 410 | 411 | class Albers(ConicProjection, Projection): 412 | """Albers Equal-Area conic projection 413 | 414 | AEA is a conic projection with an origin along the lines connecting 415 | the poles. It preserves relative area, but is not conformal, 416 | perspective or equistant. 417 | 418 | Its preferred use of for areas with predominant east-west extent 419 | at moderate latitudes. 420 | 421 | As a conic projection, it depends on two standard parallels, i.e. 422 | intersections of the cone with the sphere. To minimize scale variations, 423 | these standard parallels should be chosen as small as possible while 424 | spanning the range in declinations of the data. 425 | 426 | For details, see Snyder (1987, section 14). 427 | """ 428 | def __init__(self, lon_0, lat_0, lat_1, lat_2, lon_type="ra"): 429 | """Create Albers projection 430 | 431 | Args: 432 | lon_0: longitude that maps onto x = 0 433 | lat_0: latitude that maps onto y = 0 434 | lat_1: lower standard parallel 435 | lat_2: upper standard parallel (must not be -lat_1) 436 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 437 | """ 438 | super(Albers, self).__init__(lon_0, lat_0, lat_1, lat_2, lon_type=lon_type) 439 | 440 | # Snyder 1987, eq. 14-3 to 14-6. 441 | self.n = (np.sin(lat_1 * DEG2RAD) + np.sin(lat_2 * DEG2RAD)) / 2 442 | self.C = np.cos(lat_1 * DEG2RAD)**2 + 2 * self.n * np.sin(lat_1 * DEG2RAD) 443 | self.rho_0 = self._rho(lat_0) 444 | 445 | def _rho(self, lat): 446 | return np.sqrt(self.C - 2 * self.n * np.sin(lat * DEG2RAD)) / self.n 447 | 448 | def transform(self, lon, lat): 449 | lon_ = self._standardize(lon) 450 | # Snyder 1987, eq 14-1 to 14-4 451 | theta = self.n * lon_ 452 | rho = self._rho(lat) 453 | return rho*np.sin(theta * DEG2RAD), self.rho_0 - rho*np.cos(theta * DEG2RAD) 454 | 455 | def invert(self, x, y): 456 | # lon/lat actually x/y 457 | # Snyder 1987, eq 14-8 to 14-11 458 | rho = np.sqrt(x**2 + (self.rho_0 - y)**2) 459 | if self.n >= 0: 460 | theta = np.arctan2(x, self.rho_0 - y) / DEG2RAD 461 | else: 462 | theta = np.arctan2(-x, -(self.rho_0 - y)) / DEG2RAD 463 | lon = self._unstandardize(theta/self.n) 464 | lat = np.arcsin((self.C - (rho * self.n)**2)/(2*self.n)) / DEG2RAD 465 | return lon, lat 466 | 467 | 468 | class LambertConformal(ConicProjection, Projection): 469 | """Lambert Conformal conic projection 470 | 471 | LCC is a conic projection with an origin along the lines connecting 472 | the poles. It preserves angles, but is not equal-area, 473 | perspective or equistant. 474 | 475 | Its preferred use of for areas with predominant east-west extent 476 | at higher latitudes. 477 | 478 | As a conic projection, it depends on two standard parallels, i.e. 479 | intersections of the cone with the sphere. To minimize scale variations, 480 | these standard parallels should be chosen as small as possible while 481 | spanning the range in declinations of the data. 482 | 483 | For details, see Snyder (1987, section 15). 484 | """ 485 | def __init__(self, lon_0, lat_0, lat_1, lat_2, lon_type="ra"): 486 | """Create Lambert Conformal Conic projection 487 | 488 | Args: 489 | lon_0: longitude that maps onto x = 0 490 | lat_0: latitude that maps onto y = 0 491 | lat_1: lower standard parallel 492 | lat_2: upper standard parallel (must not be -lat_1) 493 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 494 | """ 495 | super(LambertConformal, self).__init__(lon_0, lat_0, lat_1, lat_2, lon_type=lon_type) 496 | 497 | # Snyder 1987, eq. 14-1, 14-2 and 15-1 to 15-3. 498 | self.dec_max = 89.999 499 | lat_1 *= DEG2RAD 500 | lat_2 *= DEG2RAD 501 | self.n = np.log(np.cos(lat_1)/np.cos(lat_2)) / \ 502 | (np.log(np.tan(np.pi/4 + lat_2/2)/np.tan(np.pi/4 + lat_1/2))) 503 | self.F = np.cos(lat_1)*(np.tan(np.pi/4 + lat_1/2)**self.n)/self.n 504 | self.rho_0 = self._rho(lat_0) 505 | 506 | @property 507 | def poleIsPoint(self): 508 | # because of dec_max: the pole isn't reached 509 | self._poleIsPoint = {90: False, -90: False} 510 | if self.n >= 0: 511 | self._poleIsPoint[90] = True 512 | else: 513 | self._poleIsPoint[-90] = True 514 | return self._poleIsPoint 515 | 516 | def _rho(self, lat): 517 | # check that lat is inside of -dec_max .. dec_max 518 | lat_ = np.array([lat], dtype='f8') 519 | lat_[lat_ < -self.dec_max] = -self.dec_max 520 | lat_[lat_ > self.dec_max] = self.dec_max 521 | return self.F / np.tan(np.pi/4 + lat_[0]/2 * DEG2RAD)**self.n 522 | 523 | def transform(self, lon, lat): 524 | lon_ = self._standardize(lon) 525 | theta = self.n * lon_ 526 | rho = self._rho(lat) 527 | return rho*np.sin(theta * DEG2RAD), self.rho_0 - rho*np.cos(theta * DEG2RAD) 528 | 529 | def invert(self, x, y): 530 | rho = np.sqrt(x**2 + (self.rho_0 - y)**2) * np.sign(self.n) 531 | if self.n >= 0: 532 | theta = np.arctan2(x, self.rho_0 - y) / DEG2RAD 533 | else: 534 | theta = np.arctan2(-x, -(self.rho_0 - y)) / DEG2RAD 535 | lon = self._unstandardize(theta/self.n) 536 | lat = (2 * np.arctan((self.F/rho)**(1./self.n)) - np.pi/2) / DEG2RAD 537 | return lon, lat 538 | 539 | 540 | class Equidistant(ConicProjection, Projection): 541 | """Equidistant conic projection 542 | 543 | Equistant conic is a projection with an origin along the lines connecting 544 | the poles. It preserves distances along the map, but is not conformal, 545 | perspective or equal-area. 546 | 547 | Its preferred use is for smaller areas with predominant east-west extent 548 | at moderate latitudes. 549 | 550 | As a conic projection, it depends on two standard parallels, i.e. 551 | intersections of the cone with the sphere. 552 | 553 | For details, see Snyder (1987, section 16). 554 | """ 555 | def __init__(self, lon_0, lat_0, lat_1, lat_2, lon_type="ra"): 556 | """Create Equidistant Conic projection 557 | 558 | Args: 559 | lon_0: longitude that maps onto x = 0 560 | lat_0: latitude that maps onto y = 0 561 | lat_1: lower standard parallel 562 | lat_2: upper standard parallel (must not be +-lat_1) 563 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 564 | """ 565 | super(Equidistant, self).__init__(lon_0, lat_0, lat_1, lat_2, lon_type=lon_type) 566 | 567 | # Snyder 1987, eq. 14-3 to 14-6. 568 | self.n = (np.cos(lat_1 * DEG2RAD) - np.cos(lat_2 * DEG2RAD)) / (lat_2 - lat_1) / DEG2RAD 569 | self.G = np.cos(lat_1 * DEG2RAD)/self.n + (lat_1 * DEG2RAD) 570 | self.rho_0 = self._rho(lat_0) 571 | 572 | def _rho(self, lat): 573 | return self.G - (lat * DEG2RAD) 574 | 575 | def transform(self, lon, lat): 576 | lon_ = self._standardize(lon) 577 | # Snyder 1987, eq 16-1 to 16-4 578 | theta = self.n * lon_ 579 | rho = self._rho(lat) 580 | return rho*np.sin(theta * DEG2RAD), self.rho_0 - rho*np.cos(theta * DEG2RAD) 581 | 582 | def invert(self, x, y): 583 | # Snyder 1987, eq 14-10 to 14-11 584 | rho = np.sqrt(x**2 + (self.rho_0 - y)**2) * np.sign(self.n) 585 | if self.n >= 0: 586 | theta = np.arctan2(x, self.rho_0 - y) / DEG2RAD 587 | else: 588 | theta = np.arctan2(-x, -(self.rho_0 - y)) / DEG2RAD 589 | lon = self._unstandardize(theta/self.n) 590 | lat = (self.G - rho)/ DEG2RAD 591 | return lon, lat 592 | 593 | 594 | class Hammer(Projection): 595 | """Hammer projection 596 | 597 | Hammer's 2:1 ellipse modification of the Lambert azimuthal equal-area 598 | projection. 599 | 600 | Its preferred use is for all-sky maps with an emphasis on low latitudes. 601 | It reduces the distortion at the outer meridians and has an elliptical 602 | outline. The only free parameter is the reference RA `lon_0`. 603 | 604 | For details, see Snyder (1987, section 24). 605 | """ 606 | def __init__(self, lon_0=0, lon_type="ra"): 607 | """Create Hammer projection 608 | 609 | Args: 610 | lon_0: longitude that maps onto x = 0 611 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 612 | """ 613 | super(Hammer, self).__init__(lon_0, lon_type) 614 | 615 | def transform(self, lon, lat): 616 | lon_ = self._standardize(lon) 617 | x = 2*np.sqrt(2)*np.cos(lat * DEG2RAD) * np.sin(lon_/2 * DEG2RAD) 618 | y = np.sqrt(2)*np.sin(lat * DEG2RAD) 619 | denom = np.sqrt(1+ np.cos(lat * DEG2RAD) * np.cos(lon_/2 * DEG2RAD)) 620 | return x/denom, y/denom 621 | 622 | def invert(self, x, y): 623 | dz = x*x/16 + y*y/4 624 | z = np.sqrt(1- dz) 625 | lat = np.arcsin(z*y) / DEG2RAD 626 | lon = 2*np.arctan(z*x / (2*(2*z*z - 1))) / DEG2RAD 627 | lon = self._unstandardize(lon) 628 | return lon, lat 629 | 630 | 631 | class Mollweide(Projection): 632 | """Mollweide projection 633 | 634 | Mollweide elliptical equal-area projection. It is used for all-sky maps, 635 | but it introduces strong distortions at the outer meridians. 636 | The only free parameter is the reference RA `lon_0`. 637 | 638 | For details, see Snyder (1987, section 31). 639 | """ 640 | def __init__(self, lon_0=0, lon_type="ra"): 641 | """Create Mollweide projection 642 | 643 | Args: 644 | lon_0: longitude that maps onto x = 0 645 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 646 | """ 647 | super(Mollweide, self).__init__(lon_0, lon_type) 648 | self.sqrt2 = np.sqrt(2) 649 | 650 | def transform(self, lon, lat): 651 | # Snyder p. 251 652 | lon_ = self._standardize(lon) 653 | theta_ = self.theta(lat) 654 | x = 2*self.sqrt2 / np.pi * (lon_ * DEG2RAD) * np.cos(theta_) 655 | y = self.sqrt2 * np.sin(theta_) 656 | return x, y 657 | 658 | def theta(self, lat, eps=1e-6, maxiter=100): 659 | # Snyder 1987 p. 251 660 | # Newon scheme to solve for theta given phi (=Dec) 661 | lat_ = lat * DEG2RAD 662 | t0 = lat_ 663 | mask = np.abs(lat_) < np.pi/2 664 | if mask.any(): 665 | t = t0[mask] 666 | for it in range(maxiter): 667 | f = 2*t + np.sin(2*t) - np.pi*np.sin(lat_[mask]) 668 | fprime = 2 + 2*np.cos(2*t) 669 | t_ = t - f / fprime 670 | if (np.abs(t - t_) < eps).all(): 671 | t = t_ 672 | break 673 | t = t_ 674 | t0[mask] = t 675 | return t0 676 | 677 | def invert(self, x, y): 678 | theta_ = np.arcsin(y/self.sqrt2) 679 | lon = self._unstandardize(np.pi*x/(2*self.sqrt2*np.cos(theta_)) / DEG2RAD) 680 | lat = np.arcsin((2*theta_ + np.sin(2*theta_))/np.pi) / DEG2RAD 681 | return lon, lat 682 | 683 | 684 | class EckertIV(Projection): 685 | """Eckert IV projection 686 | 687 | Eckert's IV equal-area projection is used for all-sky maps. 688 | The only free parameter is the reference RA `lon_0`. 689 | 690 | For details, see Snyder (1987, section 32). 691 | """ 692 | def __init__(self, lon_0=0, lon_type="ra"): 693 | """Create Eckert IV projection 694 | 695 | Args: 696 | lon_0: longitude that maps onto x = 0 697 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 698 | """ 699 | super(EckertIV, self).__init__(lon_0, lon_type) 700 | self.c1 = 2 / np.sqrt(4*np.pi + np.pi**2) 701 | self.c2 = 2 * np.sqrt(1/(4/np.pi + 1)) 702 | 703 | def transform(self, lon, lat): 704 | lon_ = self._standardize(lon) 705 | t = self.theta(lat) 706 | x = self.c1 * lon_ *DEG2RAD * (1 + np.cos(t)) 707 | y = self.c2 * np.sin(t) 708 | return x, y 709 | 710 | def invert(self, x, y): 711 | t = np.arcsin(y / self.c2) 712 | lon = self._unstandardize(x / (1+np.cos(t)) / self.c1 / DEG2RAD) 713 | lat = np.arcsin(y / self.c2) / DEG2RAD 714 | return lon, lat 715 | 716 | def theta(self, lat, eps=1e-6, maxiter=100): 717 | # Snyder 1993 p. 195 718 | # Newon scheme to solve for theta given phi (=Dec) 719 | lat_ = lat * DEG2RAD 720 | t = lat_ 721 | for it in range(maxiter): 722 | f = t + np.sin(t)*np.cos(t) + 2*np.sin(t) - (2+np.pi/2)*np.sin(lat_) 723 | fprime = 1 + np.cos(t)**2 - np.sin(t)**2 + 2*np.cos(t) 724 | t_ = t - f / fprime 725 | if (np.abs(t - t_) < eps).all(): 726 | t = t_ 727 | break 728 | t = t_ 729 | return t 730 | 731 | 732 | class WagnerI(Projection): 733 | """Wagner I projection 734 | 735 | Wagners's I equal-area projection is used for all-sky maps. 736 | The only free parameter is the reference RA `lon_0`. 737 | 738 | For details, see Snyder (1993, p. 204). 739 | """ 740 | def __init__(self, lon_0=0, lon_type="ra"): 741 | """Create WagnerI projection 742 | 743 | Args: 744 | lon_0: longitude that maps onto x = 0 745 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 746 | """ 747 | super(WagnerI, self).__init__(lon_0, lon_type) 748 | self.c1 = 2 / 3**0.75 749 | self.c2 = 3**0.25 750 | self.c3 = np.sqrt(3)/2 751 | 752 | def transform(self, lon, lat): 753 | lon_ = self._standardize(lon) 754 | t = np.arcsin(self.c3*np.sin(lat * DEG2RAD)) 755 | x = self.c1 * lon_ *DEG2RAD * np.cos(t) 756 | y = self.c2 * t 757 | return x, y 758 | 759 | def invert(self, x, y): 760 | t = y / self.c2 761 | lon = self._unstandardize(x / np.cos(t) / self.c1 / DEG2RAD) 762 | lat = np.arcsin(np.sin(t) / self.c3) / DEG2RAD 763 | return lon, lat 764 | 765 | 766 | class WagnerIV(Projection): 767 | """Wagner IV projection 768 | 769 | Wagner's IV equal-area projection is used for all-sky maps. 770 | The only free parameter is the reference RA `lon_0`. 771 | 772 | For details, see Snyder (1993, p. 204). 773 | """ 774 | def __init__(self, lon_0=0, lon_type="ra"): 775 | """Create WagnerIV projection 776 | 777 | Args: 778 | lon_0: longitude that maps onto x = 0 779 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 780 | """ 781 | super(WagnerIV, self).__init__(lon_0, lon_type) 782 | self.c1 = 0.86310 783 | self.c2 = 1.56548 784 | self.c3 = (4*np.pi + 3*np.sqrt(3)) / 6 785 | 786 | def transform(self, lon, lat): 787 | lon_ = self._standardize(lon) 788 | t = self.theta(lat) 789 | x = self.c1 * lon_ * DEG2RAD * np.cos(t) 790 | y = self.c2 * np.sin(t) 791 | return x, y 792 | 793 | def invert(self, x, y): 794 | t = np.arcsin(y / self.c2) 795 | lon = self._unstandardize(x / np.cos(t) / self.c1 / DEG2RAD) 796 | lat = np.arcsin(y / self.c2) / DEG2RAD 797 | return lon, lat 798 | 799 | def theta(self, lat, eps=1e-6, maxiter=100): 800 | # Newon scheme to solve for theta given phi (=Dec) 801 | lat_ = lat * DEG2RAD 802 | t0 = np.zeros(lat_.shape) 803 | mask = np.abs(lat_) < np.pi/2 804 | if mask.any(): 805 | t = t0[mask] 806 | for it in range(maxiter): 807 | f = 2*t + np.sin(2*t) - self.c3*np.sin(lat_[mask]) 808 | fprime = 2 + 2*np.cos(2*t) 809 | t_ = t - f / fprime 810 | if (np.abs(t - t_) < eps).all(): 811 | t = t_ 812 | break 813 | t = t_ 814 | t0[mask] = t 815 | t0[~mask] = np.sign(lat[~mask]) * np.pi/3 # maximum value 816 | return t0 817 | 818 | 819 | class WagnerVII(Projection): 820 | """Wagner VII projection 821 | 822 | WagnerVII equal-area projection is used for all-sky maps. 823 | The only free parameter is the reference RA `lon_0`. 824 | 825 | For details, see Snyder (1993, p. 237). 826 | """ 827 | def __init__(self, lon_0=0, lon_type="ra"): 828 | """Create WagnerVII projection 829 | 830 | Args: 831 | lon_0: longitude that maps onto x = 0 832 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 833 | """ 834 | super(WagnerVII, self).__init__(lon_0, lon_type) 835 | self.c1 = 2.66723 836 | self.c2 = 1.24104 837 | self.c3 = np.sin(65 * DEG2RAD) 838 | 839 | def transform(self, lon, lat): 840 | lon_ = self._standardize(lon) 841 | theta = np.arcsin(self.c3 * np.sin(lat * DEG2RAD)) 842 | alpha = np.arccos(np.cos(theta)*np.cos(lon_ * DEG2RAD/3)) 843 | x = self.c1 * np.cos(theta) * np.sin(lon_ * DEG2RAD / 3) / np.cos(alpha/2) 844 | y = self.c2 * np.sin(theta) / np.cos(alpha/2) 845 | return x, y 846 | 847 | 848 | class McBrydeThomasFPQ(Projection): 849 | """McBryde-Thomas Flat-Polar Quartic projection 850 | 851 | McBrydeThomasFPQ equal-area projection is used for all-sky maps. 852 | The only free parameter is the reference RA `lon_0`. 853 | 854 | For details, see Snyder (1993, p. 211). 855 | """ 856 | def __init__(self, lon_0=0, lon_type="ra"): 857 | """Create McBryde-Thomas Flat-Polar Quartic projection 858 | 859 | Args: 860 | lon_0: longitude that maps onto x = 0 861 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 862 | """ 863 | super(McBrydeThomasFPQ, self).__init__(lon_0, lon_type) 864 | self.c1 = 1 / np.sqrt(3*np.sqrt(2) + 6) 865 | self.c2 = 2 * np.sqrt(3 / (2 + np.sqrt(2))) 866 | self.c3 = 1 + np.sqrt(2) / 2 867 | 868 | def transform(self, lon, lat): 869 | lon_ = self._standardize(lon) 870 | t = self.theta(lat) 871 | x = self.c1 * lon_ * DEG2RAD * (1 + 2*np.cos(t)/np.cos(t/2)) 872 | y = self.c2 * np.sin(t/2) 873 | return x, y 874 | 875 | def invert(self, x, y): 876 | t = 2*np.arcsin(y / self.c2) 877 | lon = self._unstandardize(x / (1 + 2*np.cos(t)/np.cos(t/2)) / self.c1 / DEG2RAD) 878 | lat = np.arcsin((np.sin(t/2) + np.sin(t))/ self.c3) / DEG2RAD 879 | return lon, lat 880 | 881 | def theta(self, lat, eps=1e-6, maxiter=100): 882 | # Newon scheme to solve for theta given phi (=Dec) 883 | lat_ = lat * DEG2RAD 884 | t = lat_ 885 | for it in range(maxiter): 886 | f = np.sin(t/2) + np.sin(t) - self.c3*np.sin(lat_) 887 | fprime = np.cos(t/2)/2 + np.cos(t) 888 | t_ = t - f / fprime 889 | if (np.abs(t - t_) < eps).all(): 890 | t = t_ 891 | break 892 | t = t_ 893 | return t 894 | 895 | 896 | class HyperElliptical(Projection): 897 | """Hyperelliptical projection 898 | 899 | The outline of the map follows the equation 900 | |x/a|^k + |y/b|^k = gamma^k 901 | The parameter alpha is a weight between cylindrical equal-area (alpha=0) 902 | and sinosoidal projections. 903 | 904 | The projection does not have a closed form for either forward or backward 905 | transformation and this therefore computationally expensive. 906 | 907 | See Snyder (1993, p. 220) for details. 908 | """ 909 | def __init__(self, lon_0, alpha, k, gamma, lon_type="ra"): 910 | """Create Hyperelliptical projection 911 | 912 | Args: 913 | lon_0: longitude that maps onto x = 0 914 | alpha: cylindrical-sinosoidal weight 915 | k: hyperelliptical exponent 916 | gamma: hyperelliptical scale 917 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 918 | """ 919 | super(HyperElliptical, self).__init__(lon_0, lon_type) 920 | self.alpha = alpha 921 | self.k = k 922 | self.gamma = gamma 923 | self.gamma_pow_k = np.abs(gamma)**k 924 | self.affine = np.sqrt(2 * self.gamma / np.pi) 925 | 926 | def transform(self, lon, lat): 927 | lon_ = self._standardize(lon) 928 | y = self.Y(np.sin(np.abs(lat * DEG2RAD))) 929 | x = lon_ * DEG2RAD * (self.alpha + (1 - self.alpha) / self.gamma * self.elliptic(y)) * self.affine 930 | y *= np.sign(lat) / self.affine 931 | return x, y 932 | 933 | def invert(self, x, y): 934 | y_ = y * self.affine 935 | sinphi = self.sinPhiDiff(y_, 0) 936 | lat = np.sign(y) * np.arcsin(sinphi) / DEG2RAD 937 | 938 | lon = x / self.affine / (self.alpha + (1 - self.alpha) / self.gamma * self.elliptic(y_)) / DEG2RAD 939 | lon = self._unstandardize(lon) 940 | return lon, lat 941 | 942 | def elliptic(self, y): 943 | """Returns (gamma^k - y^k)^1/k 944 | """ 945 | y_,isArray = _toArray(y) 946 | 947 | f = (self.gamma_pow_k - y_**self.k)**(1/self.k) 948 | f[y_ < 0 ] = self.gamma 949 | 950 | if isArray: 951 | return f 952 | else: 953 | return f[0] 954 | 955 | def elliptic_scalar(self, y): 956 | """Returns (gamma^k - y^k)^1/k 957 | """ 958 | # needs to be fast for integrator, hence non-vectorized version 959 | if y < 0: 960 | return self.gamma 961 | return (self.gamma_pow_k - y**self.k)**(1/self.k) 962 | 963 | def z(self, y): 964 | """Returns int_0^y (gamma^k - y_^k)^1/k dy_ 965 | """ 966 | if hasattr(y, "__iter__"): 967 | return np.array([self.z(_) for _ in y]) 968 | 969 | f = scipy.integrate.quad(self.elliptic_scalar, 0, y)[0] 970 | 971 | # check integration errors ofat the limits 972 | lim1 = self.gamma * (self.alpha*y - 1) / (self.alpha - 1) 973 | lim2 = self.gamma * self.alpha*y / (self.alpha - 1) 974 | if f < lim2: 975 | return lim2 976 | if f > lim1: 977 | return lim1 978 | return f 979 | 980 | def sinPhiDiff(self, y, sinphi): 981 | return self.alpha*y - (self.alpha - 1) / self.gamma * self.z(y) - sinphi 982 | 983 | def Y(self, sinphi, eps=1e-5, max_iter=30): 984 | if hasattr(sinphi, "__iter__"): 985 | return np.array([self.Y(_) for _ in sinphi]) 986 | 987 | y, it, delta = 0.01, 0, 2*eps 988 | while it < max_iter and np.abs(delta) > eps: 989 | delta = self.sinPhiDiff(y, sinphi) / (self.alpha + (1 - self.alpha) / self.gamma * self.elliptic(y)) 990 | y -= delta 991 | 992 | if y >= self.gamma: 993 | return self.gamma 994 | if y <= 0: 995 | return 0. 996 | it += 1 997 | return y 998 | 999 | class Tobler(HyperElliptical): 1000 | """Tobler hyperelliptical projection 1001 | 1002 | Tobler's cylindrical equal-area projection is a specialization of 1003 | `HyperElliptical` with parameters `alpha=0`, `k=2.5`, `gamma=1.183136`. 1004 | 1005 | See Snyder (1993, p. 220) for details. 1006 | """ 1007 | def __init__(self, lon_0=0, lon_type="ra"): 1008 | """Create Tobler projection 1009 | 1010 | Args: 1011 | lon_0: longitude that maps onto x = 0 1012 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 1013 | """ 1014 | alpha, k, gamma = 0., 2.5, 1.183136 1015 | super(Tobler, self).__init__(lon_0, alpha, k, gamma, lon_type=lon_type) 1016 | 1017 | 1018 | class EqualEarth(Projection): 1019 | """Equal Earth projection 1020 | 1021 | The Equal Earth projection is a pseudo-cylindrical equal-area projection 1022 | with modest distortion. 1023 | 1024 | See https://doi.org/10.1080/13658816.2018.1504949 for details. 1025 | """ 1026 | def __init__(self, lon_0=0, lon_type="ra"): 1027 | """Create Equal Earth projection 1028 | 1029 | Args: 1030 | lon_0: longitude that maps onto x = 0 1031 | lon_type: type of longitude, "lon" or "ra" (see `BaseProjection`) 1032 | """ 1033 | super(EqualEarth, self).__init__(lon_0, lon_type) 1034 | self.A1 = 1.340264 1035 | self.A2 = -0.081106 1036 | self.A3 = 0.000893 1037 | self.A4 = 0.003796 1038 | self.sqrt3 = np.sqrt(3) 1039 | 1040 | def transform(self, lon, lat): 1041 | lon_ = self._standardize(lon) 1042 | 1043 | t = np.arcsin(self.sqrt3/2 * np.sin(lat * DEG2RAD)) 1044 | t2 = t*t 1045 | t6 = t2*t2*t2 1046 | x = 2/3*self.sqrt3 * lon_ * DEG2RAD * np.cos(t) / (self.A1 + 3*self.A2*t2 + t6*(7*self.A3 + 9*self.A4*t2)) 1047 | y = t*(self.A1 + self.A2*t2 + t6*(self.A3 + self.A4*t2)) 1048 | return x, y 1049 | -------------------------------------------------------------------------------- /skymapper/survey/__init__.py: -------------------------------------------------------------------------------- 1 | import os, numpy as np 2 | from .. import register_survey, with_metaclass, with_metaclass 3 | 4 | class BaseSurvey(object): 5 | def contains(self, ra, dec): 6 | """Whether ra, dec are inside the survey footprint""" 7 | if not hasattr(ra, '__iter__'): 8 | ra = (ra,) 9 | return np.zeros(len(ra), dtype='bool') 10 | 11 | # [blatant copy from six to avoid dependency] 12 | # python 2 and 3 compatible metaclasses 13 | # see http://python-future.org/compatible_idioms.html#metaclasses 14 | 15 | class Meta(type): 16 | def __new__(meta, name, bases, class_dict): 17 | cls = type.__new__(meta, name, bases, class_dict) 18 | 19 | # remove those that are directly derived from BaseProjection 20 | if BaseSurvey not in bases: 21 | register_survey(cls) 22 | 23 | return cls 24 | 25 | class Survey(with_metaclass(Meta, BaseSurvey)): 26 | pass 27 | 28 | try: 29 | import pymangle 30 | 31 | class MangleSurvey(pymangle.Mangle, Survey): 32 | def __init__(self, filename, verbose=False): 33 | pymangle.Mangle.__init__(self, filename, verbose=verbose) 34 | 35 | class DES(MangleSurvey): 36 | def __init__(self): 37 | # get survey polygon data 38 | this_dir, this_filename = os.path.split(__file__) 39 | MangleSurvey.__init__(self, os.path.join(this_dir, "des-round17-poly_tidy.ply")) 40 | 41 | class BOSS(MangleSurvey): 42 | def __init__(self): 43 | # get survey polygon data 44 | this_dir, this_filename = os.path.split(__file__) 45 | MangleSurvey.__init__(self, os.path.join(this_dir, "boss_survey.ply")) 46 | 47 | except ImportError: 48 | print("skymapper: survey definitions missing because pymangle is not installed") 49 | -------------------------------------------------------------------------------- /skymapper/survey/boss_survey.ply: -------------------------------------------------------------------------------- 1 | 32 polygons 2 | pixelization -1s 3 | snapped 4 | polygon 1 ( 4 caps, 1 weight, 1 pixel, 0.196803502775243530 str): 5 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.422618262 6 | 0.5890595366000000000 0.0515360316000000000 -0.8064446043000000000 1 7 | 0.2367814257000000000 0.0207156904900000000 -0.9713420698000000000 -1 8 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 9 | polygon 2 ( 4 caps, 1 weight, 1 pixel, 0.044338758080842348 str): 10 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.422618262 11 | 0.2367814257000000000 0.0207156904900000000 -0.9713420698000000000 1 12 | 0.1515445108000000000 0.0132584267100000000 -0.9883615105000000000 -1 13 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 14 | polygon 3 ( 4 caps, 1 weight, 1 pixel, 0.025586915028647751 str): 15 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.5 16 | 0.1515445108000000000 0.0132584267100000000 -0.9883615105000000000 1 17 | 0.1084526035000000000 0.0094883733380000000 -0.9940563382000000000 -1 18 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 19 | polygon 4 ( 4 caps, 1 weight, 1 pixel, 0.079605766889781425 str): 20 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.64278761 21 | 0.1084526035000000000 0.0094883733380000000 -0.9940563382000000000 1 22 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 23 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 1 24 | polygon 6 ( 4 caps, 1 weight, 1 pixel, 0.046674793737089975 str): 25 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.909961271 26 | 0.0564770543900000000 0.0049411020090000000 0.9983916706000000000 -1 27 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 28 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 1 29 | polygon 7 ( 4 caps, 1 weight, 1 pixel, 0.031731507412595572 str): 30 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.894934362 31 | 0.0564770543900000000 0.0049411020090000000 0.9983916706000000000 1 32 | 0.0954810305000000000 0.0083535077500000000 0.9953961984000000000 -1 33 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 34 | polygon 8 ( 4 caps, 1 weight, 1 pixel, 0.592780062011168644 str): 35 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.894934362 36 | 0.0954810305000000000 0.0083535077500000000 0.9953961984000000000 1 37 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 -1 38 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 39 | polygon 9 ( 4 caps, 1 weight, 1 pixel, 0.008173318451036325 str): 40 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.894934362 41 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.908508178 42 | 0.2156159959000000000 0.0188639553100000000 0.9762960071000000000 1 43 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 -1 44 | polygon 12 ( 4 caps, 1 weight, 1 pixel, 0.281601546767912236 str): 45 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.788010754 46 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 1 47 | 0.9284612980000000000 0.0812298380900000000 0.3624380383000000000 -1 48 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 49 | polygon 13 ( 4 caps, 1 weight, 1 pixel, 0.019743381358576464 str): 50 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.788010754 51 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.866025404 52 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 1 53 | 0.8754729482000000000 0.0765939581900000000 0.4771587603000000000 -1 54 | polygon 14 ( 4 caps, 1 weight, 1 pixel, 0.002041247124577867 str): 55 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.866025404 56 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.889416373 57 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 1 58 | 0.7850121348000000000 0.0686796625200000000 0.6156614753000000000 -1 59 | polygon 15 ( 4 caps, 1 weight, 1 pixel, 0.000897210587624246 str): 60 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.866025404 61 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.87630668 62 | 0.7850121348000000000 0.0686796625200000000 0.6156614753000000000 1 63 | 0.8354791732000000000 0.0730949562700000000 0.5446390350000000000 -1 64 | polygon 16 ( 4 caps, 1 weight, 1 pixel, 0.002278070413861516 str): 65 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.788010754 66 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.814115518 67 | 0.8754729482000000000 0.0765939581900000000 0.4771587603000000000 1 68 | 0.9135703839000000000 0.0799270519300000000 0.3987490689000000000 -1 69 | polygon 18 ( 4 caps, 1 weight, 1 pixel, 0.042873317160679986 str): 70 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -1.258819045 71 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.75011107 72 | 0.9284612980000000000 0.0812298380900000000 0.3624380383000000000 1 73 | 0.9563965748000000000 0.0836738581300000000 0.2798290140000000000 -1 74 | polygon 1 ( 4 caps, 1 weight, 2 pixel, 0.194401240408392469 str): 75 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.4264235636 76 | 0.5890595366000000000 0.0515360316000000000 -0.8064446043000000000 1 77 | 0.2367814257000000000 0.0207156904900000000 -0.9713420698000000000 -1 78 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 79 | polygon 2 ( 4 caps, 1 weight, 2 pixel, 0.057393262855318044 str): 80 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2568551745 81 | 0.2367814257000000000 0.0207156904900000000 -0.9713420698000000000 1 82 | 0.1515445108000000000 0.0132584267100000000 -0.9883615105000000000 -1 83 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 84 | polygon 3 ( 4 caps, 1 weight, 2 pixel, 0.028655510599030850 str): 85 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2568551745 86 | 0.1515445108000000000 0.0132584267100000000 -0.9883615105000000000 1 87 | 0.1084526035000000000 0.0094883733380000000 -0.9940563382000000000 -1 88 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 89 | polygon 4 ( 4 caps, 1 weight, 2 pixel, 0.071576009438364723 str): 90 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2568551745 91 | 0.1084526035000000000 0.0094883733380000000 -0.9940563382000000000 1 92 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 93 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 1 94 | polygon 6 ( 4 caps, 1 weight, 2 pixel, 0.049639614250452522 str): 95 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2119892464 96 | 0.0564770543900000000 0.0049411020090000000 0.9983916706000000000 -1 97 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 98 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 1 99 | polygon 7 ( 4 caps, 1 weight, 2 pixel, 0.035814614098916624 str): 100 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1748865017 101 | 0.0564770543900000000 0.0049411020090000000 0.9983916706000000000 1 102 | 0.0954810305000000000 0.0083535077500000000 0.9953961984000000000 -1 103 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 104 | polygon 8 ( 4 caps, 1 weight, 2 pixel, 0.673668090472986812 str): 105 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1464492027 106 | 0.0954810305000000000 0.0083535077500000000 0.9953961984000000000 1 107 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 -1 108 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 109 | polygon 10 ( 4 caps, 1 weight, 2 pixel, 0.003078425236534267 str): 110 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1170524071 111 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 0.1464492027 112 | 0.2156159959000000000 0.0188639553100000000 0.9762960071000000000 1 113 | 0.3160972164000000000 0.0276549230100000000 0.9483236552000000000 -1 114 | polygon 11 ( 4 caps, 1 weight, 2 pixel, 0.024841578811534415 str): 115 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1089934758 116 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 0.1464492027 117 | 0.3160972164000000000 0.0276549230100000000 0.9483236552000000000 1 118 | 0.8307126348000000000 0.0726779381900000000 0.5519369853000000000 -1 119 | polygon 12 ( 4 caps, 1 weight, 2 pixel, 0.341550176557490199 str): 120 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1464492027 121 | 0.7285706800000000000 0.0637416750800000000 0.6819983601000000000 1 122 | 0.9284612980000000000 0.0812298380900000000 0.3624380383000000000 -1 123 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 124 | polygon 17 ( 4 caps, 1 weight, 2 pixel, 0.006285118190767280 str): 125 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1170524071 126 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 0.1464492027 127 | 0.8307126348000000000 0.0726779381900000000 0.5519369853000000000 1 128 | 0.9284612980000000000 0.0812298380900000000 0.3624380383000000000 -1 129 | polygon 19 ( 4 caps, 1 weight, 2 pixel, 0.042198885269513411 str): 130 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.1613294321 131 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 0.6448930376 132 | 0.9284612980000000000 0.0812298380900000000 0.3624380383000000000 1 133 | 0.9563965748000000000 0.0836738581300000000 0.2798290140000000000 -1 134 | polygon 4 ( 4 caps, 1 weight, 3 pixel, 0.079605766827515192 str): 135 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.64278761 136 | -0.1084526035000000000 -0.0094883733380000000 -0.9940563382000000000 -1 137 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 138 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 -1 139 | polygon 5 ( 4 caps, 1 weight, 3 pixel, 0.063609346550777263 str): 140 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.64278761 141 | -0.1084526035000000000 -0.0094883733380000000 -0.9940563382000000000 1 142 | -0.1943479444000000000 -0.0170032419200000000 -0.9807852804000000000 -1 143 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 144 | polygon 6 ( 4 caps, 1 weight, 3 pixel, 0.053856536799051952 str): 145 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 1.909961271 146 | -0.0651542505800000000 -0.0057002583060000000 0.9978589232000000000 1 147 | -0.0000000000000000000 1.0000000000000000000 0.0000000000000000000 1 148 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 -1 149 | polygon 4 ( 4 caps, 1 weight, 4 pixel, 0.071576009399303977 str): 150 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2568551745 151 | -0.1084526035000000000 -0.0094883733380000000 -0.9940563382000000000 -1 152 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 153 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 -1 154 | polygon 5 ( 4 caps, 1 weight, 4 pixel, 0.053104833089896113 str): 155 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.3053416295 156 | -0.1084526035000000000 -0.0094883733380000000 -0.9940563382000000000 1 157 | -0.1943479444000000000 -0.0170032419200000000 -0.9807852804000000000 -1 158 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 159 | polygon 6 ( 4 caps, 1 weight, 4 pixel, 0.057275472412407256 str): 160 | 0.0871557427500000000 -0.9961946981000000000 0.0000000000000000612 -0.2119892464 161 | -0.0651542505800000000 -0.0057002583060000000 0.9978589232000000000 1 162 | 0.0000000000000000001 -1.0000000000000000000 0.0000000000000000000 1 163 | 0.0000000000000000000 0.0000000000000000000 1.0000000000000000000 -1 164 | -------------------------------------------------------------------------------- /skymapper/survey/des-round17-poly_tidy.ply: -------------------------------------------------------------------------------- 1 | 87 polygons 2 | real 10 3 | pixelization -1s 4 | snapped 5 | balkanized 6 | polygon 0 ( 3 caps, 1 weight, 14 pixel, 2.975265903346025961e-05 str): 7 | 0.9782555904040461354 -0.09015878560792377167 -0.1867816726045024902 1 8 | 0.5808264318940366037 -0.3651326742271353404 -0.7275429789538091023 -1 9 | -1 -2.508278806334166012e-20 0 1 10 | polygon 1 ( 4 caps, 1 weight, 14 pixel, 0.0001875388101160659245 str): 11 | -0.5974886638661998433 0.3845929749040667363 0.7036302581653400949 1 12 | 0.9764349441627021935 -0.09403533233664100887 -0.194247666885134369 1 13 | -0.5905304437350057354 0.3746840409851765883 0.7147626630239968935 -1 14 | -1 -2.508278806334166012e-20 0 1 15 | polygon 2 ( 4 caps, 1 weight, 14 pixel, 0.0001089074952181339623 str): 16 | 0.5808264318940366037 -0.3651326742271353404 -0.7275429789538091023 1 17 | 0.9772829041300194546 -0.0922082542232280175 -0.1908291464852781762 1 18 | -0.5905304437350057354 0.3746840409851765883 0.7147626630239968935 1 19 | -1 -2.508278806334166012e-20 0 1 20 | polygon 3 ( 5 caps, 1 weight, 14 pixel, 0.0002630062422928308545 str): 21 | 0 0 1 1.5 22 | 0.9757230435148338989 -0.09560651738964623218 -0.1970379054567237992 1 23 | -0.5974886638661998433 0.3845929749040667363 0.7036302581653400949 -1 24 | -1 -2.508278806334166012e-20 0 1 25 | 5.016557612668332023e-20 -1 0 -1 26 | polygon 4 ( 4 caps, 1 weight, 18 pixel, 0.0004365185007137923658 str): 27 | -0.5610153614471358414 0.4351646228481956284 0.7041970713101036343 1 28 | -0.5805393152896741202 0.4154215891644160448 0.7002849467603223257 -1 29 | 0.9747569189669560334 -0.09781990360156244684 -0.2006993158569010651 1 30 | -1 -2.508278806334166012e-20 0 1 31 | polygon 5 ( 4 caps, 1 weight, 18 pixel, 0.002598508730409268899 str): 32 | 0.9745241371884766651 -0.0983781366554436128 -0.2015550750173708079 1 33 | -0.5610153614471358414 0.4351646228481956284 0.7041970713101036343 -1 34 | -0.9612790984828945143 -0.178135445953135005 -0.2102623544883660509 -1 35 | -1 -2.508278806334166012e-20 0 1 36 | polygon 6 ( 5 caps, 1 weight, 18 pixel, 0.0003252227983806523989 str): 37 | -0.5805393152896741202 0.4154215891644160448 0.7002849467603223257 1 38 | 0.9751594796144444859 -0.09688274676416620178 -0.1991926773190125893 1 39 | -0.5754483697329967885 0.4051439100529316129 0.7104347865347376727 -1 40 | 0 0 1 -1.5 41 | -1 -2.508278806334166012e-20 0 1 42 | polygon 7 ( 22 caps, 1 weight, 18 pixel, 0.02678814975615198806 str): 43 | 0.97459819427648439 -0.09818346128720154961 -0.2012917475771934974 1 44 | -0.9612790984828945143 -0.178135445953135005 -0.2102623544883660509 1 45 | 0.9744673026900819697 -0.09851884648400679548 -0.2017610291294551936 1 46 | 0.9749130445038487176 -0.09734625092441249668 -0.20017058496991512 1 47 | 0.9754186036569963576 -0.0959469775479694411 -0.1983752130162558891 1 48 | 0.976991579683742825 -0.09127670018137464509 -0.192758961480560576 1 49 | 0.976111920856912531 -0.09394278410470542685 -0.1959088340930169304 1 50 | 0.9780479617254251024 -0.08790503360358624928 -0.1889309123248330855 1 51 | 0.9792722547555176917 -0.08376907973305705497 -0.1844141869453681357 1 52 | 0.9837920439620309253 -0.06618256158723990408 -0.1666525810744034301 1 53 | 0.9855012997731905432 -0.05839037716176917911 -0.1593039610312887775 1 54 | 0.9890281637800278995 -0.03945955876493719673 -0.1423595253995417666 1 55 | 0.9872616562568488253 -0.0495039464096560135 -0.1512077424434793159 1 56 | 0.9821640022805865288 -0.07297385126119809225 -0.173287880869634266 1 57 | 0.9806510971411347255 -0.07880931518504272618 -0.1791996582473117838 1 58 | 0.9907500856572423281 -0.02817706637367620595 -0.1327415560430886144 1 59 | 0.9923690579198947287 -0.01555271502454420676 -0.1223182976440807823 1 60 | -0.007669095215508784369 0.8787912684582982056 0.4771447280019247092 1 61 | 0.9938095926320654883 -0.001519017612297496461 -0.1110863906065075809 1 62 | -0.8852589466510170182 -0.4122166725993918959 -0.2153926930177009403 -1 63 | 0.9949876578740871634 0.01403619970452854571 -0.09900780664216870483 1 64 | -1 -2.508278806334166012e-20 0 1 65 | polygon 8 ( 3 caps, 1 weight, 18 pixel, 3.014856365176179012e-05 str): 66 | 0 0 1 -1.5 67 | -0.5754483697329967885 0.4051439100529316129 0.7104347865347376727 1 68 | 0.9757230435148338989 -0.09560651738964623218 -0.1970379054567237992 1 69 | polygon 9 ( 5 caps, 1 weight, 18 pixel, 0.001508149076290609542 str): 70 | -0.03833379638566253981 0.8779882849985140047 0.4771447280019247092 1 71 | -0.8596156450973797895 -0.4590025407127619294 -0.2244495718932092803 -1 72 | 0.9958340725093791728 0.07079861888182836304 -0.05746351532764211624 1 73 | 0.9961298627422922354 0.05009041691631958852 -0.07222358815630331136 1 74 | 0.8845500276208908062 0.4167986989272580537 0.2094041384701510814 -1 75 | polygon 10 ( 7 caps, 1 weight, 18 pixel, 0.001475544440579917242 str): 76 | -0.06895179372978902977 0.8761156080571866586 0.4771447280019247094 1 77 | 0.9894430370056151031 0.1447264768083227338 -0.007531495983866931791 1 78 | 0.9847667395893697786 0.1735324930583515508 0.01099738384687647819 1 79 | -0.7809392403387711204 -0.5641106266065671394 -0.2681661869972597114 -1 80 | 0.9926914860617945147 0.1180351239977188851 -0.02512614183015677822 1 81 | 0.9947504894276783023 0.09341753575012778166 -0.04176874187437390281 1 82 | 0.8891020384076588338 0.4157246008630625597 0.1914957480901162995 -1 83 | polygon 11 ( 5 caps, 1 weight, 18 pixel, 0.0004981329694670067615 str): 84 | -0.7809392403387711204 -0.5641106266065671394 -0.2681661869972597114 1 85 | -0.08423161771549027228 0.8747787966777662997 0.4771447280019247092 1 86 | 0.9784064622276630216 0.20444073820576266 0.03041018304830519982 1 87 | 0.6910522959083435765 0.6516765595526651354 0.312672969809366219 1 88 | 0.9700837875346030748 0.2374274685300759115 0.05065217023898277949 1 89 | polygon 12 ( 3 caps, 1 weight, 18 pixel, 0.0004551791048112998304 str): 90 | -0.05365096635976666334 0.8771855461352703297 0.4771447280019247092 1 91 | 0.8891020384076588338 0.4157246008630625597 0.1914957480901162995 1 92 | -0.8596156450973797895 -0.4590025407127619294 -0.2244495718932092803 1 93 | polygon 13 ( 4 caps, 1 weight, 18 pixel, 0.001224011132432404995 str): 94 | -0.02300494956583150868 0.8785235801248836343 0.4771447280019247092 1 95 | -0.8852589466510170182 -0.4122166725993918959 -0.2153926930177009403 1 96 | 0.8845500276208908062 0.4167986989272580537 0.2094041384701510814 1 97 | 0.9958010702694723501 0.03120922368065118341 -0.08605935630380041518 1 98 | polygon 14 ( 5 caps, 1 weight, 18 pixel, 0.0001383594837478401875 str): 99 | -0.1147096458199136099 0.8713062640058541795 0.4771447280019247091 1 100 | 0.9464902814484409575 0.3090121669635604942 0.09310009555384803921 1 101 | 0.7550774698556351073 0.5995578988399848968 0.2653080105368002132 1 102 | 0.9595294708638980594 0.272353568136634224 0.0716011764351085525 1 103 | -0.8204566124286623171 -0.5241900155496925115 -0.2282011716010610539 1 104 | polygon 15 ( 3 caps, 1 weight, 18 pixel, 0.0001157199299559904908 str): 105 | 0.6910522959083435765 0.6516765595526651354 0.312672969809366219 -1 106 | -0.09948578393446538136 0.8731755192027054054 0.4771447280019247092 1 107 | -0.8204566124286623171 -0.5241900155496925115 -0.2282011716010610539 -1 108 | polygon 16 ( 6 caps, 1 weight, 53 pixel, 0.0281932873672031451 str): 109 | 0 0 1 1.25 110 | -0.3099100836956682373 0.2649367917352378157 -0.9131069140077645539 1 111 | 0.2051112415086541009 0.01409313998391215013 0.9786371963154539875 -1 112 | -0.3147629601763935624 0.2798034467832256612 -0.9069919018763181249 1 113 | -0.3826834323650897717 0.9238795325112867562 0 1 114 | -0.7071067811865475244 0.7071067811865475244 0 -1 115 | polygon 17 ( 4 caps, 1 weight, 53 pixel, 0.001195434207930811885 str): 116 | -0.390731128489273755 0.9205048534524403274 0 1 117 | 0 0 1 1.125 118 | -0.7071067811865475244 0.7071067811865475244 0 -1 119 | 0 0 1 -1.1218866666 120 | polygon 18 ( 17 caps, 1 weight, 53 pixel, 0.00285377493930360571 str): 121 | -0.354513956254070818 0.4476082232573359966 -0.8209547693347652494 1 122 | -0.3520775938539790042 0.4324058045599439995 -0.8301003482042759141 1 123 | -0.3566941187481047822 0.4627274367063928904 -0.811574164799277039 1 124 | -0.3493838055873981981 0.4171109487968975784 -0.8390169323601395747 1 125 | -0.3464431559752421023 0.4017788737268087822 -0.8476738030072246583 1 126 | -0.3432582835185289223 0.3864146745198836636 -0.8560709375464194477 1 127 | -0.3586175535083267855 0.4777649074162988749 -0.8019564474191835682 1 128 | -0.3398331235069225531 0.371029354520923746 -0.8642052223004182957 1 129 | -0.3361849111055142225 0.3556881324854521326 -0.8720468209643269787 1 130 | -0.3323062546530827334 0.3403481481655499074 -0.8796224708127466984 1 131 | -0.3282184869715608707 0.3250818799155334851 -0.886901570728500849 1 132 | -0.3239239567950083862 0.3098836863153753676 -0.8938933779651013734 1 133 | -0.3194365790394419684 0.2947884711457010922 -0.9005887125936891836 1 134 | -0.3147629601763935624 0.2798034467832256612 -0.9069919018763181249 1 135 | -0.3826834323650897717 0.9238795325112867562 0 -1 136 | -0 1 0 1 137 | 0 0 1 1.25 138 | polygon 19 ( 5 caps, 1 weight, 53 pixel, 0.02025079720111820072 str): 139 | -0.390731128489273755 0.9205048534524403274 0 1 140 | -0.3788290581932979102 0.9229912566140603782 0.06764380889909031203 1 141 | 0.2051112415086541009 0.01409313998391215013 0.9786371963154539875 1 142 | 0 0 1 -1.125 143 | -0.7071067811865475244 0.7071067811865475244 0 -1 144 | polygon 159 ( 36 caps, 1 weight, 54 pixel, 0.01638076530598975029 str): 145 | 0 0 1 1.25 146 | 0.0363634927704583127 -0.2488905248113389504 -0.9678487500909783664 1 147 | 0.02601758256454760195 -0.2401264565425328037 -0.9703928947935594059 1 148 | 0.0469505329209232406 -0.2576503240542087689 -0.9650968645546427809 1 149 | 0.05777388109530636723 -0.2663952654633631864 -0.962130833723714344 1 150 | 0.1154796076818446688 -0.3097002711588376296 -0.9437956358522694964 1 151 | 0.1034643413070356915 -0.301120938897014726 -0.9479563862518578717 1 152 | 0.09168290124154348036 -0.2924916469377527592 -0.9518628483618710769 1 153 | 0.0801466400310188945 -0.2838275787972485303 -0.9555199744672164394 1 154 | 0.06882847012241666337 -0.2751148497918640377 -0.9589444515323125247 1 155 | 0.1402152924431168906 -0.3266846240408032675 -0.9346747178459521333 1 156 | 0.1528999750810701021 -0.3350520480844972859 -0.9297105585581954982 1 157 | 0.1277409240206110454 -0.318231380300513351 -0.9393673641991138065 1 158 | 0.1658076194973458542 -0.3433355409049905979 -0.9244612158809589038 1 159 | 0.2330968890926050355 -0.3829105236546685037 -0.8938933779651001195 1 160 | 0.2193174094313535412 -0.375286350230273409 -0.9005887125936903515 1 161 | 0.2056855426219749411 -0.3675039421381531865 -0.9069919018763181243 1 162 | 0.2470183561201418581 -0.3903684613016538017 -0.8869015707285008489 1 163 | 0.1922077767838378197 -0.3595718761736390045 -0.9131069140077648071 1 164 | 0.1789103033691944092 -0.351510853829070329 -0.91892938955542214 1 165 | 0.2751999618561192466 -0.4047212905696975591 -0.8720468209643269776 1 166 | 0.2610507089830693897 -0.397639077783752916 -0.8796224708127477759 1 167 | 0.2893999080503415674 -0.4115787008205572065 -0.8642052223004187035 1 168 | 0.006005689883686234403 -0.2225738752104719315 -0.9748973288315104708 1 169 | -0.003662096489914985693 -0.213795995766528402 -0.9768714660811304105 1 170 | -0.05101231159065032463 -0.1694423348097339526 -0.9842189996339251854 1 171 | 0.01590342395063226316 -0.2313566798026986556 -0.9727390029279786333 1 172 | 0.3610398728283392661 -0.4423612516197532011 -0.8209547693347679225 1 173 | 0.3467152650000536411 -0.4367172276508946061 -0.8301003482042754055 1 174 | 0.3753382315678206573 -0.4477371851364335214 -0.8115741647992770414 1 175 | 0.389611882953927941 -0.4528448267385837745 -0.8019564474191829801 1 176 | 0.3323556548799112137 -0.4308019334709731241 -0.8390169323601381263 1 177 | 0.3180127035461624507 -0.4246375443581586413 -0.847673803007226034 1 178 | 0.3036908207309378634 -0.418227731376300222 -0.8560709375464177175 1 179 | -1 -2.508278806334166012e-20 0 -1 180 | -0.7071067811865475244 0.7071067811865475244 0 1 181 | polygon 160 ( 25 caps, 1 weight, 61 pixel, 0.02285923881455936835 str): 182 | -0.3586175535083267855 0.4777649074162988749 -0.8019564474191835682 1 183 | -0.3826834323650897717 0.9238795325112867562 0 -1 184 | 0 0 1 1.375 185 | -0.3626037748750616235 0.6307846025092556747 -0.6860242617310235833 1 186 | -0.3614846253183750022 0.6432648777099109823 -0.6749364138667340472 1 187 | -0.3634927406790887403 0.6180195592560400021 -0.6970831025427132001 1 188 | -0.3601405390368137968 0.6554709288854757516 -0.6638197447562740189 1 189 | -0.3647117405952905831 0.5782338555168509465 -0.7298156990679708688 1 190 | -0.3646202034262257571 0.5644781361622971295 -0.7405515114076514316 1 191 | -0.3645512000059722368 0.5917462078228183701 -0.7189845951767808767 1 192 | -0.3642729560753509847 0.5505078182250130178 -0.7511633347982723772 1 193 | -0.3641427155801178843 0.6050231507427079517 -0.7080586626511279837 1 194 | -0.3636674456047034789 0.5363385267224120737 -0.7616344095171793814 1 195 | -0.3627978774741125368 0.5219223193153177293 -0.7719940367002832912 1 196 | -0.361668295938450341 0.5073689247278284692 -0.7821590745708347489 1 197 | -0.3602742721767884825 0.4926379802133973964 -0.7921554577598702676 1 198 | -0.3449891530795273564 0.7330969920456587949 -0.5861324803413284024 1 199 | 0.07361523061997953213 -0.9804135777628376702 -0.1826746133403311207 -1 200 | -0.3477199042989351244 0.7228514846923455144 -0.597140351368429277 1 201 | -0.3502768827837075046 0.7123118764267158121 -0.6082087602861208179 1 202 | -0.3526436549892339772 0.7015170306635626114 -0.6192869353416171246 1 203 | -0.3548218326369408423 0.6904287340817012473 -0.6304043386894654051 1 204 | -0.3568046350149900678 0.6790418635785722594 -0.6415548300336919641 1 205 | -0.3585775495761024079 0.6674026251385436631 -0.6526835963146142882 1 206 | 0 0 1 -1.25 207 | polygon 161 ( 4 caps, 1 weight, 61 pixel, 2.214523437835125286e-05 str): 208 | -0.3420820220659924302 0.7430855073587154586 -0.5751554737048777337 1 209 | 0.07361523061997953213 -0.9804135777628376702 -0.1826746133403311207 1 210 | 0 0 1 1.375 211 | -0.7071067811865475244 0.7071067811865475244 0 -1 212 | polygon 162 ( 4 caps, 1 weight, 61 pixel, 0.09817477042468103921 str): 213 | 0 0 1 -1.25 214 | -0.3826834323650897717 0.9238795325112867562 0 1 215 | 0 0 1 1.5 216 | -0.7071067811865475244 0.7071067811865475244 0 -1 217 | polygon 163 ( 14 caps, 1 weight, 61 pixel, 0.01727536387678464073 str): 218 | 0.01155547107937379145 0.9783106972286601812 0.2068203345324306945 1 219 | -0.3091516522349230846 0.8213380540913923161 -0.479404898620969753 1 220 | 0 0 1 1.4375 221 | -0.3133552615462693533 0.8136055605549046471 -0.4897493970342470105 1 222 | -0.321381604303897418 0.7974502617012593415 -0.5106730309382375181 1 223 | -0.3174372243764953395 0.8056368672138470087 -0.5001828133452855813 1 224 | -0.3251989335588757231 0.7890095025951608965 -0.5212577658190875563 1 225 | -0.3288706821212469018 0.780340017001304177 -0.5318961668479197468 1 226 | -0.3324048082950741856 0.771406331153932396 -0.5426226273184196742 1 227 | -0.3357810051643322691 0.7622425038012144362 -0.5533872802745706965 1 228 | -0.3390171982162043078 0.7527789042476619839 -0.5642616951675208204 1 229 | -0.3420820220659924302 0.7430855073587154586 -0.5751554737048777337 1 230 | 0 0 1 -1.375 231 | -0.3826834323650897717 0.9238795325112867562 0 -1 232 | polygon 164 ( 4 caps, 1 weight, 61 pixel, 2.575185625939419021e-08 str): 233 | 0.01155547107937379145 0.9783106972286601812 0.2068203345324306945 -1 234 | -0.3091516522349230846 0.8213380540913923161 -0.479404898620969753 1 235 | 0 0 1 1.4375 236 | -0.7071067811865475244 0.7071067811865475244 0 -1 237 | polygon 165 ( 4 caps, 1 weight, 61 pixel, 1.352417020645136497e-05 str): 238 | -0.07176852066165196901 -0.9748188822522267362 -0.2111336691448271599 1 239 | -0.2659184508633210383 0.8818754892549196126 -0.3893366139240537411 1 240 | 0 0 1 1.5 241 | -0.7071067811865475244 0.7071067811865475244 0 -1 242 | polygon 166 ( 12 caps, 1 weight, 61 pixel, 0.02027099200040909793 str): 243 | -0.2863029917111579803 0.8568645349179340502 -0.428735076401631948 1 244 | 0 0 1 1.5 245 | -0.2911091545700031091 0.8501586181716897167 -0.4387320185192108011 1 246 | -0.2957984683705700159 0.8432594235426231359 -0.448794842568614881 1 247 | -0.2813797534089614813 0.8633845905097356391 -0.4187988577370384111 1 248 | -0.2763403377108781762 0.8697238549262038253 -0.4089210607515115051 1 249 | -0.2711817151863402114 0.8758914499822217962 -0.3990922765434526825 1 250 | -0.07176852066165196901 -0.9748188822522267362 -0.2111336691448271599 -1 251 | -0.3003677047357229709 0.8361638742684372948 -0.4589217986979813663 1 252 | 0 0 1 -1.4375 253 | -0.3048264573820734175 0.828847314733172424 -0.4691406609317768284 1 254 | -0.3826834323650897717 0.9238795325112867562 0 -1 255 | polygon 167 ( 26 caps, 1 weight, 62 pixel, 0.02283996679692032548 str): 256 | -0.1182597570330291291 -0.2855043093120859368 -0.9510530580523452987 1 257 | -0.9238795325112867562 0.3826834323650897717 0 1 258 | 0 0 1 1.375 259 | -0.1132590082535185739 -0.2875247429678213539 -0.9510530580523452985 1 260 | -0.1082237596829293434 -0.2894575938015340447 -0.9510530580523452988 1 261 | -0.1031555451067535937 -0.2913022730481361146 -0.9510530580523452985 1 262 | -0.09805590835224812435 -0.2930582188004592374 -0.9510530580523452985 1 263 | -0.0929264028181696444 -0.2947248961804170283 -0.9510530580523452988 1 264 | -0.08776859100159447415 -0.2963017975019340872 -0.9510530580523452984 1 265 | -0.07737434114252040488 -0.299184378104945843 -0.9510530580523452986 1 266 | -0.08258404402196678298 -0.2977884424255920912 -0.9510530580523452987 1 267 | -0.07214106928921994139 -0.3004891793244646783 -0.9510530580523452987 1 268 | 0.8438236415697316934 -0.2178353914799564591 -0.4904175814005561197 1 269 | -0.04567719312893218624 -0.3056332357534837107 -0.9510530580523452986 1 270 | -0.05100427172228808424 -0.3047895093919102187 -0.9510530580523452987 1 271 | -0.05631581392038790262 -0.3038529411932136747 -0.9510530580523452985 1 272 | -0.06161020177602402747 -0.3028238164451401422 -0.9510530580523452986 1 273 | -0.06688582256736775025 -0.3017024486290572391 -0.9510530580523452987 1 274 | 0.839958545139676612 -0.2198849727185293094 -0.4961050707455120024 1 275 | -0.0349829217136810872 -0.3070411632951790126 -0.9510530580523452986 1 276 | -0.04033620082006430845 -0.3063838632707110552 -0.9510530580523452986 1 277 | 0.8362442443141070494 -0.2216940652185488668 -0.5015449185251524822 1 278 | -0.02961898647045670624 -0.3076049356069327213 -0.9510530580523452986 1 279 | 0.8326769415747721413 -0.223281254213011644 -0.5067490429065929394 1 280 | -0.02424602899702786335 -0.3080750084754646362 -0.9510530580523452986 1 281 | -0.7071067811865475244 0.7071067811865475244 0 1 282 | polygon 168 ( 6 caps, 1 weight, 62 pixel, 0.04681602061975176409 str): 283 | -0.9238795325112867562 0.3826834323650897717 0 1 284 | 0 0 1 -1.375 285 | 0.9782555904040461354 -0.09015878560792377167 -0.1867816726045024902 1 286 | -0.6509870250452962536 0.3282661366403678721 0.6844393594452923906 -1 287 | -1 -2.508278806334166012e-20 0 -1 288 | 0 0 1 1.5 289 | polygon 169 ( 4 caps, 1 weight, 62 pixel, 0.0008712710158988536992 str): 290 | -0.5935175198472120156 0.3347738006205031935 0.7318903306182681539 1 291 | 0 0 1 -1.375 292 | 0.9805067287059733985 -0.0855598358592203513 -0.176878685686133344 1 293 | 0.8497109348848580648 -0.2144379285674439531 -0.481671778215004312 1 294 | polygon 170 ( 4 caps, 1 weight, 62 pixel, 0.0008858910515287202886 str): 295 | -0.6509870250452962536 0.3282661366403678721 0.6844393594452923906 1 296 | 0.9793355971649652282 -0.08793009670291403443 -0.1821265664842917331 1 297 | -0.5935175198472120156 0.3347738006205031935 0.7318903306182681539 -1 298 | 0 0 1 -1.375 299 | polygon 171 ( 12 caps, 1 weight, 62 pixel, 0.01985345570402982001 str): 300 | 0 0 1 -1.25 301 | 0 0 1 1.3125 302 | 0.389611882953927941 -0.4528448267385837745 -0.8019564474191829801 1 303 | 0.4318415398042248197 -0.4664097895614089416 -0.7719940367002832862 1 304 | 0.4178702038155851384 -0.4621814306412728397 -0.7821590745708366702 1 305 | 0.4457347229404394346 -0.4703547416627524085 -0.7616344095171745449 1 306 | 0.4594434526656370357 -0.4739887744002084671 -0.7511633347982759622 1 307 | 0.4037822448232041911 -0.4576566720881335049 -0.7921554577598696537 1 308 | 0.9092285082461920997 -0.3986703977532492938 0.1198558874134691204 1 309 | 0.4864284821391921686 -0.4803710827659054234 -0.7298156990679723976 1 310 | 0.4730133086410064684 -0.477327842054425273 -0.7405515114076492082 1 311 | -0.7071067811865475244 0.7071067811865475244 0 1 312 | polygon 172 ( 5 caps, 1 weight, 62 pixel, 0.0004371833320257500523 str): 313 | 0 0 1 1.3125 314 | 0.4046850777349186754 -0.4610561368814111886 -0.789719713887574329 1 315 | 0.9092285082461920997 -0.3986703977532492938 0.1198558874134691204 -1 316 | -0.8989805996013738513 0.4299535887358879323 0.08350923944980752968 -1 317 | -0.7071067811865475244 0.7071067811865475244 0 1 318 | polygon 173 ( 5 caps, 1 weight, 62 pixel, 9.040728879171648288e-05 str): 319 | -0.1182597570330291291 -0.2855043093120859368 -0.9510530580523452987 1 320 | -0.9238795325112867562 0.3826834323650897717 0 -1 321 | 0 0 1 1.3125 322 | -0.8989805996013738513 0.4299535887358879323 0.08350923944980752968 1 323 | -0.1232244827449394351 -0.2833969082779917106 -0.9510530580523452987 1 324 | polygon 174 ( 4 caps, 1 weight, 62 pixel, 0.07363107781851077919 str): 325 | 0 0 1 -1.3125 326 | -0.9238795325112867562 0.3826834323650897717 0 -1 327 | 0 0 1 1.5 328 | -0.7071067811865475244 0.7071067811865475244 0 1 329 | polygon 175 ( 4 caps, 1 weight, 69 pixel, 0.05606731278115361771 str): 330 | -0.3826834323650897717 0.9238795325112867562 0 1 331 | 0 0 1 -1.5 332 | -0.7071067811865475244 0.7071067811865475244 0 -1 333 | 0 0 1 1.6427742396 334 | polygon 176 ( 4 caps, 1 weight, 69 pixel, 2.478802301802019575e-06 str): 335 | -0.2129540128767412821 0.9289252348144874906 -0.3029001428268761742 1 336 | 0 0 1 1.5625 337 | 0.1402898565464654183 0.9653999581361378721 0.2198219210654738094 -1 338 | -0.7071067811865475244 0.7071067811865475244 0 -1 339 | polygon 177 ( 13 caps, 1 weight, 69 pixel, 0.02289019174640377066 str): 340 | -0.2256293366362898952 0.9194709740613688801 -0.321969766139159853 1 341 | 0 0 1 1.5625 342 | -0.2317663681379886892 0.9145365900837836524 -0.3315225120531813444 1 343 | -0.2377586598776192743 0.9094736496208174505 -0.3410696414203250797 1 344 | 0.1402898565464654183 0.9653999581361378721 0.2198219210654738094 1 345 | -0.2193530423600041518 0.9242713914765324793 -0.3124206102443381825 1 346 | -0.2436428493790276586 0.904250321836570323 -0.3506700976771863761 1 347 | -0.249391941434051365 0.8988889478896437369 -0.3602808889042049099 1 348 | -0.2550218688712756743 0.8933734144442961693 -0.3699294375438939138 1 349 | -0.2605228331737395875 0.8877114292453498642 -0.379600147236939377 1 350 | -0.2659184508633210383 0.8818754892549196126 -0.3893366139240537411 1 351 | 0 0 1 -1.5 352 | -0.3826834323650897717 0.9238795325112867562 0 -1 353 | polygon 178 ( 7 caps, 1 weight, 69 pixel, 0.03147595198746825328 str): 354 | -0 1 0 1 355 | -0.1997047633290714142 0.9378495659991628596 -0.2838242397309670252 1 356 | -0.2064052370285160242 0.9334515439400146545 -0.293368528038037864 1 357 | -0.2129540128767412821 0.9289252348144874906 -0.3029001428268761742 1 358 | 0 0 1 -1.5625 359 | -0.3826834323650897717 0.9238795325112867562 0 -1 360 | 0 0 1 1.6427742396 361 | polygon 181 ( 4 caps, 1 weight, 70 pixel, 0.09817477042468103878 str): 362 | -0.9238795325112867562 0.3826834323650897717 0 1 363 | -1 -2.508278806334166012e-20 0 -1 364 | 0 0 1 -1.5 365 | 0 0 1 1.75 366 | polygon 182 ( 4 caps, 1 weight, 70 pixel, 0.05606731278115361771 str): 367 | -0.9238795325112867562 0.3826834323650897717 0 -1 368 | 0 0 1 -1.5 369 | -0.7071067811865475244 0.7071067811865475244 0 1 370 | 0 0 1 1.6427742396 371 | polygon 183 ( 4 caps, 1 weight, 70 pixel, 0.03275024483626834751 str): 372 | -0.9238795325112867562 0.3826834323650897717 0 -1 373 | 0 0 1 1.75 374 | 0 0 1 -1.6427742396 375 | -0.7660444431045086803 0.642787609703783232 0 1 376 | polygon 185 ( 8 caps, 1 weight, 75 pixel, 0.0003676427039060021183 str): 377 | 0.7837026830846380248 0.6192002137795975956 -0.04900203854167755681 1 378 | 0.2746738380424027456 0.8242419242147510837 -0.4951560693981448486 -1 379 | 0.7825528348269605458 0.620513211749350145 -0.05073869084627583362 1 380 | 0.7814362577827328667 0.6217747630664636853 -0.05247398437474720587 1 381 | 0.7803532438980031147 0.6229851886818072884 -0.05420580615539848893 1 382 | 0.7793040400584781211 0.6241448702783928159 -0.05593204853840177553 1 383 | 0.7782888476286337654 0.6252542495829623999 -0.0576506117516532287 1 384 | 0.8090169943800236364 0.5877852522854863223 0 -1 385 | polygon 186 ( 5 caps, 1 weight, 75 pixel, 0.0002723215031075806175 str): 386 | 0.2746738380424027456 0.8242419242147510837 -0.4951560693981448486 1 387 | 0.784885466067701117 0.6178355095524482597 -0.04726614318679083553 1 388 | 0.7861008033211065816 0.6164189013475964085 -0.04553312068520138482 1 389 | 0.2765145199232919271 0.8369534604966942075 -0.4722802401479442805 -1 390 | 0.8090169943800236364 0.5877852522854863223 0 -1 391 | polygon 187 ( 5 caps, 1 weight, 75 pixel, 0.0001474091197667735768 str): 392 | 0.751424123009144506 0.6535363538403073495 -0.09084063831267730222 1 393 | 0.2765145199232919271 0.8369534604966942075 -0.4722802401479442805 1 394 | 1 1.836685814385754238e-19 0 1 395 | 0 0 1 1.75 396 | 0.8090169943800236364 0.5877852522854863223 0 -1 397 | polygon 200 ( 4 caps, 1 weight, 76 pixel, 2.985960451439353958e-07 str): 398 | -0.3860482701745327561 0.7703373141177441367 -0.5074910398944113011 1 399 | 0 0 1 1.625 400 | -0.1887236370021774384 -0.9603678815476440137 -0.2051265973206808686 1 401 | 0.7071067811865475244 0.7071067811865475243 0 1 402 | polygon 201 ( 10 caps, 1 weight, 76 pixel, 0.0006587471401563928936 str): 403 | -0.1561957080460712063 0.9614729021325811538 -0.2262139678550785452 1 404 | 0 0 1 1.625 405 | -0.1638718994995142687 0.9578634161372099671 -0.2358891192496492058 1 406 | -0.1887236370021774384 -0.9603678815476440137 -0.2051265973206808686 -1 407 | -0.1713717415429413647 0.9541210857290142678 -0.2455293871776376815 1 408 | -0.1786972127811472458 0.9502491751773424397 -0.2551348882827077147 1 409 | -0.1858582193095585226 0.9462462355468916354 -0.2647164219092131057 1 410 | -0.1928667608177309242 0.9421084284641198205 -0.2742883912755743994 1 411 | -0.1997047633290714142 0.9378495659991628596 -0.2838242397309670252 1 412 | -1.003311522533666405e-19 1 0 -1 413 | polygon 202 ( 5 caps, 1 weight, 76 pixel, 0.00069787539974846928 str): 414 | -0.3860482701745327561 0.7703373141177441367 -0.5074910398944113011 1 415 | 0.1658976920750430225 0.9766470023662778176 0.136523216095790603 1 416 | 0 0 1 -1.625 417 | -1.003311522533666405e-19 1 0 -1 418 | 0 0 1 1.6427742396 419 | polygon 251 ( 4 caps, 1 weight, 78 pixel, 0.04908738521234051961 str): 420 | -0.9238795325112867562 0.3826834323650897717 0 1 421 | 0 0 1 1.875 422 | -1 -2.508278806334166012e-20 0 -1 423 | 0 0 1 -1.75 424 | polygon 252 ( 14 caps, 1 weight, 78 pixel, 0.01060637063409674858 str): 425 | 0.8903998444541764817 0.3496841351530694085 0.2913916996385247321 1 426 | 0.8838978737184212109 0.362422335303455315 0.2955919479776871683 1 427 | 0.9091973819109558543 -0.4153146650343472297 -0.02956094947175145928 1 428 | 0 0 1 -1.875 429 | 0.8368244999300553002 0.4454131883748649804 0.3183266371803495819 1 430 | -0.285680653364680642 0.9233059025191060701 0.2566958797224999913 1 431 | 0.8436687352133285686 0.4341959215996163402 0.3157482650622205846 1 432 | 0.331616979440534452 -0.9116033167529712776 -0.2429188585345280826 -1 433 | -0.8314696123025452371 0.5555702330196022248 0 1 434 | 0.850495028313745824 0.4227518516588324757 0.3129521987982017528 1 435 | 0.8572616378271088679 0.411138580075266409 0.3099476605552566145 1 436 | 0.8640060256942163159 0.3992780904671441841 0.3067092989085858154 1 437 | 0.8706963624812543475 0.3872104830384806187 0.3032422895755449374 1 438 | 0.8773340752235071626 0.3749174958153666985 0.2995359607513728384 1 439 | polygon 253 ( 4 caps, 1 weight, 78 pixel, 0.003653016677198943872 str): 440 | 0 0 1 -1.875 441 | 0.9091973819109558543 -0.4153146650343472297 -0.02956094947175145928 -1 442 | 0.7939866043362251772 0.503330923592934388 0.3409446487184015853 1 443 | -0.9612790984828945143 -0.178135445953135005 -0.2102623544883660509 -1 444 | polygon 254 ( 4 caps, 1 weight, 78 pixel, 0.001094900019572747716 str): 445 | 0.6322062338060577336 0.6659534873834254083 0.3960066042108567149 1 446 | 0 0 1 -1.875 447 | -0.9612790984828945143 -0.178135445953135005 -0.2102623544883660509 1 448 | 0.9517784968148518775 0.2255309268046759881 0.2079747437923289676 1 449 | polygon 255 ( 5 caps, 1 weight, 78 pixel, 0.0007368422188229581567 str): 450 | 0.7901012731403826234 0.5194995103560173729 0.3253617016826115175 1 451 | 0.5676727994442887221 0.712330346105286443 0.4127142725768121303 1 452 | 0 0 1 -1.875 453 | 0.9517784968148518775 0.2255309268046759881 0.2079747437923289676 -1 454 | 0.9428734990727063328 0.267828718515206234 0.1981346569505034261 1 455 | polygon 256 ( 4 caps, 1 weight, 78 pixel, 6.643459503551758447e-05 str): 456 | -0.9185524199354849451 -0.3278768494958842946 -0.2208126431962575791 1 457 | 0.06895179372978903022 0.8761156080571866587 0.4771447280019247093 1 458 | 0 0 1 -1.875 459 | -0.9154829002395093874 -0.3382564247761947768 -0.2178844888161683419 -1 460 | polygon 257 ( 4 caps, 1 weight, 78 pixel, 6.789458606340460431e-05 str): 461 | 0.896846221894102346 0.3776816097183032653 0.2302682261078714054 1 462 | 0 0 1 -1.875 463 | 0.05365096635976666336 0.8771855461352703297 0.4771447280019247092 1 464 | -0.9154829002395093874 -0.3382564247761947768 -0.2178844888161683419 1 465 | polygon 258 ( 4 caps, 1 weight, 78 pixel, 6.877064065410800481e-05 str): 466 | 0.08423161771549027228 0.8747787966777662996 0.4771447280019247093 1 467 | -0.9185524199354849451 -0.3278768494958842946 -0.2208126431962575791 -1 468 | 0 0 1 -1.875 469 | 0.9428734990727063328 0.267828718515206234 0.1981346569505034261 -1 470 | polygon 259 ( 4 caps, 1 weight, 78 pixel, 6.792555634374574172e-05 str): 471 | 0.02300494956583150882 0.8785235801248836343 0.4771447280019247093 1 472 | 0 0 1 -1.875 473 | -0.8899090410279831679 -0.3972870642573473736 -0.2241090968042879553 -1 474 | 0.9098379026628598528 0.3572575257098126809 0.2110972553155319716 -1 475 | polygon 260 ( 4 caps, 1 weight, 78 pixel, 4.995586119880971246e-05 str): 476 | -0.8899090410279831679 -0.3972870642573473736 -0.2241090968042879553 1 477 | 0.007669095215508784369 0.8787912684582982056 0.4771447280019247092 1 478 | 0 0 1 -1.875 479 | -1 -2.508278806334166012e-20 0 -1 480 | polygon 261 ( 4 caps, 1 weight, 78 pixel, 6.489800100310848599e-05 str): 481 | 0.03833379638566253956 0.8779882849985140047 0.4771447280019247091 1 482 | 0.9098379026628598528 0.3572575257098126809 0.2110972553155319716 1 483 | 0.896846221894102346 0.3776816097183032653 0.2302682261078714054 -1 484 | 0 0 1 -1.875 485 | polygon 262 ( 4 caps, 1 weight, 78 pixel, 0.02454369260617026002 str): 486 | 0 0 1 -1.75 487 | -0.9238795325112867562 0.3826834323650897717 0 -1 488 | -0.8314696123025452371 0.5555702330196022248 0 1 489 | 0 0 1 1.875 490 | polygon 263 ( 4 caps, 1 weight, 78 pixel, 8.872458573697117323e-05 str): 491 | 0.331616979440534452 -0.9116033167529712776 -0.2429188585345280826 1 492 | -0.8314696123025452371 0.5555702330196022248 0 1 493 | -0.5013186642144762786 0.8470630751100467015 0.1765325570406902631 1 494 | 0.4963770038114039104 0.7831320408578434516 0.374585206153476545 1 495 | polygon 264 ( 4 caps, 1 weight, 78 pixel, 0.01704423098002436207 str): 496 | 0 0 1 -1.75 497 | -0.8314696123025452371 0.5555702330196022248 0 -1 498 | 0 0 1 1.90625 499 | -0.7660444431045086803 0.642787609703783232 0 1 500 | polygon 265 ( 4 caps, 1 weight, 78 pixel, 1.564128341220803048e-06 str): 501 | 0.5756134144735115894 0.7000628271470270895 0.4225887304756919685 1 502 | 0 0 1 -1.90625 503 | -0.9221227666073517571 0.3618187764424811329 -0.1370283777838347585 -1 504 | -0.7660444431045086803 0.642787609703783232 0 1 505 | polygon 266 ( 4 caps, 1 weight, 78 pixel, 0.0001632227997010187066 str): 506 | -0.4584061339005848028 0.8673287340621325885 0.1939192705032112114 1 507 | -0.2243183206344415421 0.9352524126926616171 0.2738324589604001587 1 508 | -0.8314696123025452371 0.5555702330196022248 0 -1 509 | 0.441196038227868621 0.8012081947486631808 0.4042418638879715337 -1 510 | polygon 267 ( 3 caps, 1 weight, 78 pixel, 8.779041208264223156e-06 str): 511 | -0.2243183206344415421 0.9352524126926616171 0.2738324589604001587 -1 512 | -0.5013186642144762786 0.8470630751100467015 0.1765325570406902631 1 513 | -0.8314696123025452371 0.5555702330196022248 0 -1 514 | polygon 268 ( 5 caps, 1 weight, 78 pixel, 0.0003956934475495576566 str): 515 | 0.441196038227868621 0.8012081947486631808 0.4042418638879715337 1 516 | -0.8314696123025452371 0.5555702330196022248 0 -1 517 | -0.1075009072558786072 0.9436969490228947147 0.3128571932112845642 1 518 | -0.9221103948023519495 0.367601305954839091 -0.1207547086359260818 1 519 | 0 0 1 -1.90625 520 | polygon 269 ( 4 caps, 1 weight, 78 pixel, 6.078029907679097232e-05 str): 521 | -0.9221227666073517571 0.3618187764424811329 -0.1370283777838347585 1 522 | -0.06551528585756879962 0.9436446959401509811 0.3244109047843815132 1 523 | -0.9221103948023519495 0.367601305954839091 -0.1207547086359260818 -1 524 | 0 0 1 -1.90625 525 | polygon 281 ( 4 caps, 1 weight, 83 pixel, 0.0002844759868370371793 str): 526 | 0.751424123009144506 0.6535363538403073495 -0.09084063831267730222 1 527 | -0.8952653480414983436 -0.4198165585359000711 -0.149177792500117212 -1 528 | 0 0 1 -1.75 529 | 0.8090169943800236364 0.5877852522854863223 0 -1 530 | polygon 282 ( 3 caps, 1 weight, 83 pixel, 6.440326455403816777e-05 str): 531 | 0.5002281896288337394 -0.7557623537295104344 0.4226050437298280743 1 532 | -0.9501851678888793667 -0.2784048287856304637 -0.1401388526884172772 1 533 | 0.7923449319877641979 0.608955310085284109 -0.03691800471640911289 1 534 | polygon 283 ( 5 caps, 1 weight, 83 pixel, 0.0004539294517276384194 str): 535 | 0.5262991899032826192 -0.7378442516691842866 0.4226050437298280749 1 536 | 0.9451782186152896758 0.2902995607823079428 0.1495469828008133813 -1 537 | 0.7853298924191419094 0.6176204919957558604 -0.04244865062471911209 1 538 | 0.788823584815617185 0.613339555663690298 -0.03964771742365191506 1 539 | -0.9645219079131150548 0.01470955035728006249 -0.2635927128809350151 -1 540 | polygon 284 ( 3 caps, 1 weight, 83 pixel, 6.404762585809822134e-05 str): 541 | 0.5133418742199290705 -0.7469170617856313389 0.4226050437298280738 1 542 | -0.9645219079131150548 0.01470955035728006249 -0.2635927128809350151 1 543 | -0.9501851678888793667 -0.2784048287856304637 -0.1401388526884172772 -1 544 | polygon 285 ( 5 caps, 1 weight, 83 pixel, 0.0006101179653738883317 str): 545 | 0.7818664730650331085 0.6217967215313988 -0.04531727473767539738 1 546 | -0.9618623547134485598 -0.1858215709854527594 -0.200726566108221964 -1 547 | 0.9398990854414962898 0.3023047825642886301 0.1587498901574329605 -1 548 | 0.7784359002141839059 0.6258669895369607023 -0.04824998099144744442 1 549 | 0.8090169943800236364 0.5877852522854863223 0 -1 550 | polygon 286 ( 3 caps, 1 weight, 83 pixel, 5.15930367077001989e-05 str): 551 | -0.9578549065847782189 -0.1823172585454477415 -0.2219783664412776833 1 552 | 0.9514490243543450531 0.2405570103487825976 0.1920340564254700532 1 553 | 0.8090169943800236364 0.5877852522854863223 0 -1 554 | polygon 287 ( 3 caps, 1 weight, 83 pixel, 7.581122445932076889e-05 str): 555 | 0.9514490243543450531 0.2405570103487825976 0.1920340564254700532 -1 556 | 0.9398990854414962898 0.3023047825642886301 0.1587498901574329605 1 557 | 0.8090169943800236364 0.5877852522854863223 0 -1 558 | polygon 288 ( 3 caps, 1 weight, 83 pixel, 0.0001390695427042616499 str): 559 | -0.9618623547134485598 -0.1858215709854527594 -0.200726566108221964 1 560 | 0.9451782186152896758 0.2902995607823079428 0.1495469828008133813 1 561 | 0.8090169943800236364 0.5877852522854863223 0 -1 562 | polygon 289 ( 5 caps, 1 weight, 83 pixel, 0.005971438621579211199 str): 563 | 0.8596559617951757712 0.5105572301101675008 -0.01797059075948163619 1 564 | 0.8171641074640062413 -0.3708014594000068196 0.4413038626381817127 1 565 | -0.7087358218777298066 -0.6837257789724501719 0.1738177032290297593 -1 566 | 0.8468550732577544234 0.5314836823350838542 0.01901526516099798257 -1 567 | 0.1816503933493989197 0.8394936303474596179 -0.512106999758889721 1 568 | polygon 290 ( 4 caps, 1 weight, 83 pixel, 0.0006533407784938527994 str): 569 | -0.7087358218777298066 -0.6837257789724501719 0.1738177032290297593 1 570 | 0.8237987987883575179 0.5634685796842770719 -0.0621184257965601052 1 571 | 0.0345026885196880547 0.824233687737279589 -0.5651976578896254313 1 572 | 0.8047444926431332935 0.5874527457694846662 -0.08535556835051360726 1 573 | polygon 291 ( 3 caps, 1 weight, 83 pixel, 3.723960162238483182e-05 str): 574 | -0.9600842122292270639 -0.128231423164017487 -0.2485860163836959785 1 575 | -0.9578549065847782189 -0.1823172585454477415 -0.2219783664412776833 -1 576 | 0.8090169943800236364 0.5877852522854863223 0 -1 577 | polygon 292 ( 3 caps, 1 weight, 83 pixel, 2.803429125199617677e-05 str): 578 | -0.9590780145299260461 -0.078617899619938955 -0.2720084335175734332 1 579 | -0.9600842122292270639 -0.128231423164017487 -0.2485860163836959785 -1 580 | 0.8090169943800236364 0.5877852522854863223 0 -1 581 | polygon 293 ( 5 caps, 1 weight, 83 pixel, 0.004514647038873632282 str): 582 | -0.9590780145299260461 -0.078617899619938955 -0.2720084335175734332 -1 583 | 0.8893620613372665026 0.4372079599014090621 0.1337322835099143177 -1 584 | 0.1442939056508521343 0.8369925573040381902 -0.527847258029886264 1 585 | 0.8468550732577544234 0.5314836823350838542 0.01901526516099798257 1 586 | 0.8090169943800236364 0.5877852522854863223 0 -1 587 | polygon 294 ( 3 caps, 1 weight, 83 pixel, 2.851040495134527994e-05 str): 588 | 0.8893620613372665026 0.4372079599014090621 0.1337322835099143177 1 589 | -0.8952653480414983436 -0.4198165585359000711 -0.149177792500117212 1 590 | 0.8090169943800236364 0.5877852522854863223 0 -1 591 | polygon 986 ( 2 caps, 1 weight, 325 pixel, 3.6049283281582023e-08 str): 592 | 0.9017856173594472696 0.09119894469017802021 0.4224517165440784495 1 593 | 0 0 1 -1.906381534 594 | polygon 1 ( 4 caps, 1 weight, 0 pixel, 0.1641679187268640892 str): 595 | 0 0 1 1.1218693434 596 | 0 0 1 -0.9128442573 597 | 0 1 0 1 598 | -0.7071067812 0.7071067812 0 -1 599 | polygon 2 ( 4 caps, 1 weight, 0 pixel, 0.05238355672736196922 str): 600 | 0 0 1 1.0348994967 601 | 0 0 1 -0.9651005033 602 | 0.6819983601 0.7313537016 0 1 603 | 0 1 0 -1 604 | polygon 3 ( 4 caps, 1 weight, 0 pixel, 0.248361915934263672 str): 605 | 0 0 1 1.906307787 606 | 0 0 1 -1.6427876097 607 | 0.8090169944 0.5877852523 0 1 608 | 0 1 0 -1 609 | polygon 4 ( 4 caps, 1 weight, 0 pixel, 0.229964736962300422 str): 610 | 0 0 1 1.906307787 611 | 0 0 1 -1.6427876097 612 | 0 1 0 1 613 | -0.7660444431 0.6427876097 0 -1 614 | --------------------------------------------------------------------------------