quad_sphere 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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