quad_sphere 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2012 Cesar Rincon <crincon@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,153 @@
1
+ QuadSphere
2
+ ==========
3
+
4
+ QuadSphere is a small Ruby gem that implements a projection of
5
+ spherical to planar coordinates, called the _quadrilateralised
6
+ spherical cube_. It is useful for handling geographic or astronomical
7
+ data, or for general mapmaking.
8
+
9
+ ![Sphere picture][9] ![Grid picture][10]
10
+
11
+ Background
12
+ ----------
13
+
14
+ The quadrilateralised spherical cube, or "quad sphere" for short, is a
15
+ projection of the sphere onto the sides of an inscribed cube, where
16
+ the distortion of the tangential (gnomonic) projection is compensated
17
+ by a further curvilinear transformation. This makes it approximately
18
+ equal-area, with no singularities at the poles, or anywhere;
19
+ distortion is moderate over the entire sphere. This makes it
20
+ well-suited for storing data collected on a spherical surface, like
21
+ the Earth or the celestial sphere, as rasters of pixels: each
22
+ equal-area pixel then corresponds to an equal-area region on the
23
+ sphere, so numerical analysis can be performed on the pixels rather
24
+ than the original surface.
25
+
26
+ This projection was proposed in 1975 in the report "Feasibility Study
27
+ of a Quadrilateralized Spherical Cube Earth Data Base", by F. K. Chan
28
+ and E. M. O'Neill ([citation entry][6]), and it was used to hold data
29
+ for the Cosmic Background Explorer project (COBE). The quad sphere,
30
+ along with a binning scheme for storing pixels along a Z-order curve,
31
+ became the [COBE Sky Cube][1] format.
32
+
33
+ This is not a Sky Cube reader, though — neither the binning scheme nor
34
+ the FITS format are implemented here. You should use a [FITS][5] library
35
+ if you need to read COBE data. And, for current astronomical work,
36
+ the quadrilateralised spherical cube projection has been superseded by
37
+ [HEALPix][3], so you should use that instead. This implementation was
38
+ only created because this author had a very specific need involving
39
+ storage and manipulation of spherical data — for a game, no less.
40
+
41
+ Note also that this is _not_ the projection by Laubscher and O'Neill,
42
+ 1976, which is similar to this but introduces singularities, making it
43
+ non-differentiable along the diagonals.
44
+
45
+ As Chan's original report is not readily available, this
46
+ implementation is based on formulae found in [FITS WCS documents][2].
47
+ Specifically: "Representations of celestial coordinates in FITS (Paper
48
+ II)", Calabretta, M. R., and Greisen, E. W., _Astronomy &
49
+ Astrophysics_, 395, 1077-1122, 2002.
50
+
51
+ Finally, bear in mind that this is not an exact projection, it's
52
+ accuracy is limited — see discussion in the documentation of
53
+ [`QuadSphere::CSC.forward_distort`][8].
54
+
55
+ Examples
56
+ --------
57
+
58
+ The basic usage, for converting a tuple of spherical coordinates (φ,θ)
59
+ to cartesian (x,y) on a cube face, is:
60
+
61
+ require 'quad_sphere/csc'
62
+ face, x, y = QuadSphere::CSC.forward(phi, theta)
63
+
64
+ Parameters `phi` and `theta` should be given in radians. `phi` is the
65
+ azimuthal angle, or longitude; you'll want to make it something
66
+ between -π and π (or 0 and 2π, if you like). `theta` is the elevation
67
+ angle, or (geocentric) latitude, so it should be between -π/2 and π/2.
68
+ The values returned are: a face identifier (see constants in module
69
+ [QuadSphere][11]), and cartesian (x,y), with each coordinate between
70
+ -1 and 1.
71
+
72
+ The inverse transfomation looks, not very surprisingly, like this:
73
+
74
+ lon, lat = QuadSphere::CSC.inverse(face, x, y)
75
+
76
+ With all symbols meaning the same as before.
77
+
78
+ As a more practical example, suppose you're storing geographic data in
79
+ six bitmaps of 100x100 pixels each. The following function will give
80
+ you the bitmap and specific coordinates of the pixel where you should
81
+ store a given latitude and longitude.
82
+
83
+ def geographic_to_storage_bin(latitude, longitude)
84
+ # Convert both angles to radians.
85
+ latitude = latitude*Math::PI/180
86
+ longitude = longitude*Math::PI/180
87
+
88
+ # Geographic latitudes are normally geodetic; we convert this to
89
+ # geocentric because we want spherical coordinates. The magic
90
+ # number below is the Earth's eccentricity, squared, using the WGS84
91
+ # ellipsoid.
92
+ latitude = Math.atan((1 - 6.69437999014e-3) * Math.tan(latitude))
93
+
94
+ # Apply the forward transformation...
95
+ face, x, y = QuadSphere::CSC.forward(longitude, latitude)
96
+
97
+ # ... then adjust x and y so they become integer coordinates on a
98
+ # 100x100 grid, with 0,0 being top-left, as used in pictures.
99
+ x = (100*(1+x)/2).floor
100
+ y = 99 - (100*(1+y)/2).floor
101
+
102
+ # And return the computed values.
103
+ [ face, x, y ]
104
+ end
105
+
106
+ Trying the above on a few locations on Earth:
107
+
108
+ [ [ 'Accra', 5.5500, -0.2167 ],
109
+ [ 'Buenos Aires', -34.6036, -58.3817 ],
110
+ [ 'Cairo', 30.0566, -31.2262 ],
111
+ [ 'Honolulu', 21.3069, -157.8583 ],
112
+ [ 'Kuala Lumpur', 3.1597, 101.7000 ],
113
+ [ 'London', 51.5171, -0.1062 ],
114
+ [ 'Longyearbyen', 78.216667, 15.55 ],
115
+ [ 'New Delhi', 28.6667, 77.2167 ],
116
+ [ 'New York', 40.7142, -74.0064 ],
117
+ [ 'Quito', -0.2186, -78.5097 ],
118
+ [ 'Sydney', -33.8683, 151.2086 ],
119
+ [ 'Ushuaia', -54.8000, -68.3000 ] ].each do |city, lat, lon|
120
+ face, x, y = geographic_to_storage_bin(lat, lon)
121
+ puts '%-12s - bitmap %d, x=%2d, y=%2d' % [city, face, x, y]
122
+ end
123
+
124
+ Gives you:
125
+
126
+ Accra - bitmap 1, x=49, y=43
127
+ Buenos Aires - bitmap 4, x=85, y=93
128
+ Cairo - bitmap 1, x=14, y=11
129
+ Honolulu - bitmap 3, x=76, y=23
130
+ Kuala Lumpur - bitmap 2, x=63, y=46
131
+ London - bitmap 0, x=49, y=93
132
+ Longyearbyen - bitmap 0, x=53, y=63
133
+ New Delhi - bitmap 2, x=35, y=16
134
+ New York - bitmap 4, x=67, y= 3
135
+ Quito - bitmap 4, x=63, y=50
136
+ Sydney - bitmap 3, x=17, y=92
137
+ Ushuaia - bitmap 5, x=11, y=32
138
+
139
+ See more code in the `examples` directory, including the programs that
140
+ created the graphics above. And see the [API reference][7] for the
141
+ nitty gritty.
142
+
143
+ [1]: http://lambda.gsfc.nasa.gov/product/cobe/skymap_info_new.cfm
144
+ [2]: http://fits.gsfc.nasa.gov/fits_wcs.html
145
+ [3]: http://healpix.jpl.nasa.gov/
146
+ [4]: http://lambda.gsfc.nasa.gov/product/cobe/skymap_info_new.cfm
147
+ [5]: http://en.wikipedia.org/wiki/FITS
148
+ [6]: http://www.dtic.mil/docs/citations/ADA010232
149
+ [7]: http://rubydoc.info/github/crinc/QuadSphere/master/frames
150
+ [8]: http://rubydoc.info/github/crinc/QuadSphere/master/QuadSphere/CSC.forward_distort
151
+ [9]: https://raw.github.com/crinc/QuadSphere/master/examples/sphere.png
152
+ [10]: https://raw.github.com/crinc/QuadSphere/master/examples/grid.png
153
+ [11]: http://rubydoc.info/github/crinc/QuadSphere/master/QuadSphere
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # The example from the README.
4
+
5
+ require 'quad_sphere/csc'
6
+
7
+ def geographic_to_storage_bin(latitude, longitude)
8
+ # Convert both angles to radians.
9
+ latitude = latitude*Math::PI/180
10
+ longitude = longitude*Math::PI/180
11
+
12
+ # Geographic latitudes are normally geodetic; we convert this to
13
+ # geocentric because we want spherical coordinates. The magic
14
+ # number below is the Earth's eccentricity, squared, using the WGS84
15
+ # ellipsoid.
16
+ latitude = Math.atan((1 - 6.69437999014e-3) * Math.tan(latitude))
17
+
18
+ # Apply the forward transformation...
19
+ face, x, y = QuadSphere::CSC.forward(longitude, latitude)
20
+
21
+ # ... then adjust x and y so they become integer coordinates on a
22
+ # 100x100 grid, with 0,0 being top-left, as used in pictures.
23
+ x = (100*(1+x)/2).floor
24
+ y = 99 - (100*(1+y)/2).floor
25
+
26
+ # And return the computed values.
27
+ [ face, x, y ]
28
+ end
29
+
30
+ [ [ 'Accra', 5.5500, -0.2167 ],
31
+ [ 'Buenos Aires', -34.6036, -58.3817 ],
32
+ [ 'Cairo', 30.0566, -31.2262 ],
33
+ [ 'Honolulu', 21.3069, -157.8583 ],
34
+ [ 'Kuala Lumpur', 3.1597, 101.7000 ],
35
+ [ 'London', 51.5171, -0.1062 ],
36
+ [ 'Longyearbyen', 78.216667, 15.55 ],
37
+ [ 'New Delhi', 28.6667, 77.2167 ],
38
+ [ 'New York', 40.7142, -74.0064 ],
39
+ [ 'Quito', -0.2186, -78.5097 ],
40
+ [ 'Sydney', -33.8683, 151.2086 ],
41
+ [ 'Ushuaia', -54.8000, -68.3000 ] ].each do |city, lat, lon|
42
+ face, x, y = geographic_to_storage_bin(lat, lon)
43
+ puts '%-12s - bitmap %d, x=%2d, y=%2d' % [city, face, x, y]
44
+ end
@@ -0,0 +1,108 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Compute the closure error introduced by the curvilinear distortion
3
+ # in QuadSphere::CSC. This requires two additional gems: Joseph
4
+ # Ruscio's Aggregate, and Willem van Bergen's ChunkyPNG.
5
+
6
+ require 'quad_sphere/csc'
7
+ require 'aggregate'
8
+ require 'chunky_png'
9
+
10
+ # Some routines to manipulate colours. We should really pack this
11
+ # stuff in a gem one day...
12
+ module Colour
13
+
14
+ # We expect an array of stops. A gradient stop is an array of 4
15
+ # elements: a min value, a max value, a start colour, and an end
16
+ # colour.
17
+ #
18
+ # We expect the min of a stop to be always less than the max, and
19
+ # we expect all stops to have a max equal or smaller than the min
20
+ # of the next stop. We expect all colours to be arrays of three
21
+ # floats.
22
+ #
23
+ # We don't validate any of this; just expect breakage if you don't
24
+ # conform.
25
+ def self.gradient_map(stops, value, rgbout)
26
+ stops.each do |min, max, start, finish|
27
+ if value >= min && value <= max
28
+ t = (value - min) / (max - min)
29
+ lerp(t, start, finish, rgbout)
30
+ return true
31
+ end
32
+ end
33
+
34
+ # If we're here, v is out of range. We'll just hardcode a
35
+ # value.
36
+ rgbout[0] = 1.0
37
+ rgbout[1] = 0.0
38
+ rgbout[2] = 0.0
39
+ false
40
+ end
41
+
42
+ # Performs a linear interpolation between two colours.
43
+ def self.lerp(t, rgb1, rgb2, rgbout)
44
+ rgbout[0] = rgb1[0] + t*(rgb2[0] - rgb1[0])
45
+ rgbout[1] = rgb1[1] + t*(rgb2[1] - rgb1[1])
46
+ rgbout[2] = rgb1[2] + t*(rgb2[2] - rgb1[2])
47
+ end
48
+
49
+ # Converts as expected by chunkypng
50
+ def self.rgb1_to_i(rgb)
51
+ r = (rgb[0]*256).floor
52
+ g = (rgb[1]*256).floor
53
+ b = (rgb[2]*256).floor
54
+
55
+ (r < 0 ? 0 : r > 255 ? 255 : r) << 24 |
56
+ (g < 0 ? 0 : g > 255 ? 255 : g) << 16 |
57
+ (b < 0 ? 0 : b > 255 ? 255 : b) << 8 | 0xff
58
+ end
59
+ end # module Colour
60
+
61
+ # Size of the pretty pic. Computation time depends on the square of
62
+ # this, so keep it reasonable.
63
+ grid = 200
64
+
65
+ image = ChunkyPNG::Image.new(grid,grid)
66
+ stats = Aggregate.new
67
+
68
+ # The expected maximum error is just below 2.4e-4, so we'll set a B&W
69
+ # gradient white to that.
70
+ stops = [ [ 0.0, 2.4e-4, [0.0,0.0,0.0], [1.0,1.0,1.0] ] ]
71
+ rgb = Array.new(3)
72
+
73
+ # We're mapping the range -1.0 to 1.0, inclusive, to 0 to grid-1.
74
+ # Which is to say, if grid is 100, we want -1.0 at grid 0, and 1.0 at
75
+ # grid 1...
76
+ d = 2.0/(grid-1)
77
+
78
+ # Perform grid² transformations of χ,ψ to x,y and back to χ',ψ', and
79
+ # measuring the closure error.
80
+ grid.times do |row|
81
+ grid.times do |col|
82
+ chi = col*d - 1.0
83
+ psi = row*d - 1.0
84
+ x = QuadSphere::CSC.forward_distort(chi,psi)
85
+ y = QuadSphere::CSC.forward_distort(psi,chi)
86
+ chi1 = QuadSphere::CSC.inverse_distort(x,y)
87
+ psi1 = QuadSphere::CSC.inverse_distort(y,x)
88
+ error = Math::sqrt((chi1-chi)**2 + (psi1-psi)**2)
89
+
90
+ # since we're dealing with very small errors, and Aggregate only
91
+ # works with integers, we scale the error for it.
92
+ stats << error*1e8
93
+
94
+ Colour.gradient_map(stops, error, rgb)
95
+ image[col,row] = Colour.rgb1_to_i(rgb)
96
+ end
97
+ end
98
+
99
+ # Print statistics:
100
+ puts("samples: %d mean: %8g min: %8g max: %8g std_dev: %8g" %
101
+ [ stats.count,
102
+ stats.mean/1e8, stats.min/1e8, stats.max/1e8, stats.std_dev/1e8 ])
103
+
104
+ # Print histogram:
105
+ puts stats.to_s
106
+
107
+ # Write the pretty pic:
108
+ image.save('distort-error.png')
@@ -0,0 +1,164 @@
1
+ # A wireframe view of the CSC projection. You need OpenGL for this.
2
+
3
+ require 'quad_sphere/csc'
4
+ require 'opengl'
5
+
6
+ class CSCWireframeApp
7
+
8
+ WINDOW_SIZE = 480
9
+
10
+ # How many subdivisions of each cube face. Can be raised for
11
+ # smoother shading, at the cost of model complexity.
12
+ FACE_SUBDIV = 20
13
+
14
+ def run
15
+ @ang1 = 0
16
+ @ang2 = 0
17
+ @ang3 = 0
18
+
19
+ glutInit
20
+ glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
21
+ glutInitWindowSize(WINDOW_SIZE, WINDOW_SIZE)
22
+ glutCreateWindow('CSC')
23
+
24
+ glEnable(GL_DEPTH_TEST)
25
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
26
+ glClearColor(1, 1, 1, 0.0)
27
+ setup_model
28
+
29
+ glutDisplayFunc(method :display)
30
+ glutReshapeFunc(method :reshape)
31
+ glutKeyboardFunc(method :keyboard)
32
+
33
+ # And run.
34
+ glutMainLoop()
35
+ end
36
+
37
+ private
38
+
39
+ def setup_model
40
+ glGenLists(1)
41
+ glNewList(1, GL_COMPILE)
42
+
43
+ # The cube faces, and the colour we want each:
44
+ faces = [ [ QuadSphere::TOP_FACE, [ 0.9, 0.25, 0.25, 1.0 ] ], # red
45
+ [ QuadSphere::FRONT_FACE, [ 0.25, 0.25, 0.9, 1.0 ] ], # blue
46
+ [ QuadSphere::EAST_FACE, [ 0.25, 0.8, 0.25, 1.0 ] ], # green
47
+ [ QuadSphere::BACK_FACE, [ 0.8, 0.75, 0.15, 1.0 ] ], # yellow
48
+ [ QuadSphere::WEST_FACE, [ 0.25, 0.9, 0.9, 1.0 ] ], #cyan
49
+ [ QuadSphere::BOTTOM_FACE, [ 0.9, 0.25, 0.9, 1.0 ] ] ] #magenta
50
+
51
+ faces.each do |face, colour|
52
+ glColor3fv(colour);
53
+ # Calculate all the vertices we'll use for this face...
54
+ vertices = grid(face, FACE_SUBDIV)
55
+ # ... and arrange them in triangle strips:
56
+ mesh2quads(FACE_SUBDIV, vertices)
57
+ end
58
+
59
+ glEndList
60
+ end
61
+
62
+ def reshape(w, h)
63
+ glViewport(0, 0, w, h)
64
+ glMatrixMode(GL_PROJECTION)
65
+ glLoadIdentity()
66
+ gluPerspective(60.0, w.to_f / h.to_f, 1.0, 20.0)
67
+ glMatrixMode(GL_MODELVIEW)
68
+ glLoadIdentity()
69
+ gluLookAt(2.2,0,0, 0,0,0, 0,0,1)
70
+ end
71
+
72
+ def display
73
+ # XXX - maybe it'd be more efficient to only clear the depth
74
+ # buffer if we moved the model or camera?
75
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
76
+
77
+ # Rotate and draw the model.
78
+ glPushMatrix()
79
+ glRotate(@ang1, 0, 0, 1)
80
+ glRotate(@ang2, 1, 0, 0)
81
+ glRotate(@ang3, 0, 1, 0)
82
+ glCallList(1)
83
+ glPopMatrix()
84
+
85
+ # Done.
86
+ glutSwapBuffers()
87
+ end
88
+
89
+ def keyboard(key, x, y)
90
+ case (key)
91
+ when ?s
92
+ @ang1 = (@ang1 + 5) % 360
93
+ glutPostRedisplay()
94
+ when ?a
95
+ @ang1 = (@ang1 - 5) % 360
96
+ glutPostRedisplay()
97
+ when ?w
98
+ @ang2 = (@ang2 + 5) % 360
99
+ glutPostRedisplay()
100
+ when ?q
101
+ @ang2 = (@ang2 - 5) % 360
102
+ glutPostRedisplay()
103
+ when ?x
104
+ @ang3 = (@ang3 + 5) % 360
105
+ glutPostRedisplay()
106
+ when ?z
107
+ @ang3 = (@ang3 - 5) % 360
108
+ glutPostRedisplay()
109
+ when ?r
110
+ @ang1 = @ang2 = @ang3 = 0
111
+ glutPostRedisplay()
112
+ when ?\e, ?Q
113
+ exit(0)
114
+ end
115
+ end
116
+
117
+ # Create a NxN grid of points on a face of the cube. Note that this
118
+ # will generate (N+1)*(N+1) points.
119
+ def grid(face, n)
120
+ dx = 2.0/n
121
+ dy = 2.0/n
122
+ a = Array.new
123
+ n += 1
124
+
125
+ n.times do |j|
126
+ y = -1.0 + j*dy
127
+ n.times do |i|
128
+ x = -1.0 + i*dx
129
+ lon, lat = QuadSphere::CSC.inverse(face, x, y)
130
+ sx = Math::cos(lat) * Math::cos(lon)
131
+ sy = Math::cos(lat) * Math::sin(lon)
132
+ sz = Math::sin(lat)
133
+ a << [sx,sy,sz]
134
+ end
135
+ end
136
+
137
+ a
138
+ end
139
+
140
+ # p grid(0, 3)
141
+ #
142
+ # Create quad strips to represent a NxN mesh. The given array
143
+ # should then contain (N+1)**2 points, arranged as N+1 rows of N+1
144
+ # points.
145
+
146
+ def mesh2quads(n,a)
147
+ dx = 2.0/n
148
+ dy = 2.0/n
149
+ row = n+1
150
+
151
+ n.times do |j|
152
+ glBegin(GL_QUAD_STRIP)
153
+ rowi = j*row
154
+ row.times do |x|
155
+ glVertex3fv(a[rowi+x])
156
+ glVertex3fv(a[rowi+row+x])
157
+ end
158
+ glEnd
159
+ end
160
+ end
161
+
162
+ end
163
+
164
+ CSCWireframeApp.new.run