ffi-gdal 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +25 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +60 -0
  7. data/Rakefile +57 -0
  8. data/ffi-gdal.gemspec +28 -0
  9. data/lib/ext/cpl_error_symbols.rb +37 -0
  10. data/lib/ext/to_bool.rb +13 -0
  11. data/lib/ffi/gdal/cpl_conv.rb +151 -0
  12. data/lib/ffi/gdal/cpl_error.rb +91 -0
  13. data/lib/ffi/gdal/cpl_string.rb +113 -0
  14. data/lib/ffi/gdal/cpl_vsi.rb +119 -0
  15. data/lib/ffi/gdal/gdal_color_entry.rb +13 -0
  16. data/lib/ffi/gdal/gdal_gcp.rb +18 -0
  17. data/lib/ffi/gdal/ogr_api.rb +28 -0
  18. data/lib/ffi/gdal/ogr_core.rb +199 -0
  19. data/lib/ffi/gdal/ogr_srs_api.rb +48 -0
  20. data/lib/ffi/gdal/version.rb +5 -0
  21. data/lib/ffi/gdal.rb +607 -0
  22. data/lib/ffi-gdal/color_table.rb +59 -0
  23. data/lib/ffi-gdal/dataset.rb +347 -0
  24. data/lib/ffi-gdal/driver.rb +151 -0
  25. data/lib/ffi-gdal/exceptions.rb +17 -0
  26. data/lib/ffi-gdal/geo_transform.rb +137 -0
  27. data/lib/ffi-gdal/major_object.rb +71 -0
  28. data/lib/ffi-gdal/raster_attribute_table.rb +78 -0
  29. data/lib/ffi-gdal/raster_band.rb +571 -0
  30. data/lib/ffi-gdal/version_info.rb +48 -0
  31. data/lib/ffi-gdal.rb +12 -0
  32. data/linkies.rb +35 -0
  33. data/meow.rb +144 -0
  34. data/readie.rb +90 -0
  35. data/rubby.rb +224 -0
  36. data/spec/ext/cpl_error_symbols_spec.rb +79 -0
  37. data/spec/ffi-gdal/integration/color_table_info_spec.rb +60 -0
  38. data/spec/ffi-gdal/integration/dataset_info_spec.rb +95 -0
  39. data/spec/ffi-gdal/integration/driver_info_spec.rb +60 -0
  40. data/spec/ffi-gdal/integration/geo_transform_info_spec.rb +66 -0
  41. data/spec/ffi-gdal/integration/raster_attribute_table_info_spec.rb +23 -0
  42. data/spec/ffi-gdal/integration/raster_band_info_spec.rb +333 -0
  43. data/spec/ffi-gdal/unit/version_info_spec.rb +48 -0
  44. data/spec/ffi-gdal_spec.rb +6 -0
  45. data/spec/spec_helper.rb +13 -0
  46. data/spec/support/integration_help.rb +1 -0
  47. data/spec/support/shared_examples/major_object_examples.rb +68 -0
  48. data/things.rb +84 -0
  49. metadata +216 -0
@@ -0,0 +1,347 @@
1
+ require_relative '../ffi/gdal'
2
+ require_relative '../ffi-gdal'
3
+ require_relative 'driver'
4
+ require_relative 'geo_transform'
5
+ require_relative 'raster_band'
6
+ require_relative 'exceptions'
7
+ require_relative 'major_object'
8
+
9
+
10
+ module GDAL
11
+
12
+ # A set of associated raster bands and info common to them all. It's also
13
+ # responsible for the georeferencing transform and coordinate system
14
+ # definition of all bands.
15
+ class Dataset
16
+ include FFI::GDAL
17
+ include MajorObject
18
+
19
+ ACCESS_FLAGS = {
20
+ 'r' => :GA_ReadOnly,
21
+ 'w' => :GA_Update
22
+ }
23
+
24
+ # @param path [String] Path to the file that contains the dataset.
25
+ # @param access_flag [String] 'r' or 'w'.
26
+ def self.open(path, access_flag)
27
+ file_path = ::File.expand_path(path)
28
+ pointer = FFI::GDAL.GDALOpen(file_path, ACCESS_FLAGS[access_flag])
29
+ raise OpenFailure.new(file_path) if pointer.null?
30
+
31
+ new(pointer)
32
+ end
33
+
34
+ # Computes NDVI from the red and near-infrared bands in the dataset. Raises
35
+ # a GDAL::RequiredBandNotFound if one of those band types isn't found.
36
+ #
37
+ # @param source [String] Path to the dataset that contains the red and NIR
38
+ # bands.
39
+ # @param destination [String] Path to output the new dataset to.
40
+ # @param driver_name [String] The type of dataset to create.
41
+ def self.extract_ndvi(source, destination, driver_name: 'GTiff')
42
+ extract_8bit(source, destination, driver_name) do |original, ndvi_dataset|
43
+ red = original.red_band
44
+ nir = original.undefined_band
45
+
46
+ if red.nil?
47
+ fail RequiredBandNotFound, 'Red band not found.'
48
+ elsif nir.nil?
49
+ fail RequiredBandNotFound, 'Near-infrared'
50
+ end
51
+
52
+ the_array = original.calculate_ndvi(red.to_a, nir.to_a)
53
+
54
+ ndvi_band = ndvi_dataset.raster_band(1)
55
+ ndvi_band.write_array(the_array)
56
+ end
57
+ end
58
+
59
+ def self.extract_gndvi(source, destination, driver_name: 'GTiff')
60
+ extract_8bit(source, destination, driver_name) do |original, gndvi_dataset|
61
+ green = original.green_band
62
+ nir = original.undefined_band
63
+
64
+ if green.nil?
65
+ fail RequiredBandNotFound, 'Green band not found.'
66
+ elsif nir.nil?
67
+ fail RequiredBandNotFound, 'Near-infrared'
68
+ end
69
+
70
+ the_array = original.calculate_ndvi(green.to_a, nir.to_a)
71
+
72
+ gndvi_band = gndvi_dataset.raster_band(1)
73
+ gndvi_band.write_array(the_array)
74
+ end
75
+ end
76
+
77
+ def self.extract_nir(source, destination, driver_name: 'GTiff')
78
+ extract_8bit(source, destination, driver_name) do |original, nir_dataset|
79
+ nir = original.undefined_band
80
+ fail RequiredBandNotFound, 'Near-infrared' if nir.nil?
81
+
82
+ nir_band = nir_dataset.raster_band(1)
83
+ nir_band.write_array(nir.to_a)
84
+ end
85
+ end
86
+
87
+ def self.extract_natural_color(source, destination, driver_name: 'GTiff')
88
+ original_dataset = open(source, 'r')
89
+ geo_transform = original_dataset.geo_transform
90
+ projection = original_dataset.projection
91
+ rows = original_dataset.raster_y_size
92
+ columns = original_dataset.raster_x_size
93
+
94
+ driver = GDAL::Driver.by_name(driver_name)
95
+ driver.create_dataset(destination, columns, rows, bands: 3) do |new_dataset|
96
+ new_dataset.geo_transform = geo_transform
97
+ new_dataset.projection = projection
98
+ original_red_band = original_dataset.red_band
99
+ original_green_band = original_dataset.green_band
100
+ original_blue_band = original_dataset.blue_band
101
+
102
+ new_red_band = new_dataset.raster_band(1)
103
+ new_red_band.write_array(original_red_band.to_a)
104
+
105
+ new_green_band = new_dataset.raster_band(2)
106
+ new_green_band.write_array(original_green_band.to_a)
107
+
108
+ new_blue_band = new_dataset.raster_band(3)
109
+ new_blue_band.write_array(original_blue_band.to_a)
110
+ end
111
+ end
112
+
113
+ def self.extract_8bit(source, destination, driver_name)
114
+ dataset = open(source, 'r')
115
+ geo_transform = dataset.geo_transform
116
+ projection = dataset.projection
117
+ rows = dataset.raster_y_size
118
+ columns = dataset.raster_x_size
119
+
120
+ driver = GDAL::Driver.by_name(driver_name)
121
+ driver.create_dataset(destination, columns, rows) do |new_dataset|
122
+ new_dataset.geo_transform = geo_transform
123
+ new_dataset.projection = projection
124
+
125
+ yield dataset, new_dataset
126
+ end
127
+ end
128
+ private_class_method :extract_8bit
129
+
130
+ # @param dataset_pointer [FFI::Pointer] Pointer to the dataset in memory.
131
+ def initialize(dataset_pointer)
132
+ @gdal_dataset = dataset_pointer
133
+ @last_known_file_list = []
134
+ @open = true
135
+ close_me = -> { self.close }
136
+ ObjectSpace.define_finalizer self, close_me
137
+ end
138
+
139
+ # @return [FFI::Pointer] Pointer to the GDALDatasetH that's represented by
140
+ # this Ruby object.
141
+ def c_pointer
142
+ @gdal_dataset
143
+ end
144
+
145
+ # Close the dataset.
146
+ def close
147
+ @last_known_file_list = file_list
148
+ GDALClose(@gdal_dataset)
149
+ @open = false
150
+ end
151
+
152
+ # Tries to reopen the dataset using the first item from #file_list before
153
+ # the dataset was closed.
154
+ #
155
+ # @param access_flag [String]
156
+ # @return [Boolean]
157
+ def reopen(access_flag)
158
+ @gdal_dataset = GDALOpen(@last_known_file_list.first, access_flag)
159
+
160
+ @open = true unless @gdal_dataset.null?
161
+ end
162
+
163
+ # @return [Boolean]
164
+ def open?
165
+ @open
166
+ end
167
+
168
+ # @return [GDAL::Driver] The driver to be used for working with this
169
+ # dataset.
170
+ def driver
171
+ return @driver if @driver
172
+
173
+ @driver = if @gdal_dataset && !null?
174
+ Driver.new(dataset: @gdal_dataset)
175
+ else
176
+ Driver.new
177
+ end
178
+ end
179
+
180
+ # Fetches all files that form the dataset.
181
+ # @return [Array<String>]
182
+ def file_list
183
+ list_pointer = GDALGetFileList(c_pointer)
184
+ file_list = list_pointer.get_array_of_string(0)
185
+ CSLDestroy(list_pointer)
186
+
187
+ file_list
188
+ end
189
+
190
+ # @return [Fixnum]
191
+ def raster_x_size
192
+ return nil if null?
193
+
194
+ GDALGetRasterXSize(@gdal_dataset)
195
+ end
196
+
197
+ # @return [Fixnum]
198
+ def raster_y_size
199
+ return nil if null?
200
+
201
+ GDALGetRasterYSize(@gdal_dataset)
202
+ end
203
+
204
+ # @return [Fixnum]
205
+ def raster_count
206
+ return 0 if null?
207
+
208
+ GDALGetRasterCount(@gdal_dataset)
209
+ end
210
+
211
+ # @param raster_index [Fixnum]
212
+ # @return [GDAL::RasterBand]
213
+ def raster_band(raster_index)
214
+ @raster_bands ||= Array.new(raster_count)
215
+
216
+ if @raster_bands[raster_index] && !@raster_bands[raster_index].null?
217
+ return @raster_bands[raster_index]
218
+ end
219
+
220
+ @raster_bands[raster_index] =
221
+ GDAL::RasterBand.new(@gdal_dataset, band_id: raster_index)
222
+ end
223
+
224
+ # @return [String]
225
+ def projection
226
+ return '' if null?
227
+
228
+ GDALGetProjectionRef(@gdal_dataset)
229
+ end
230
+
231
+ # @param new_projection [String]
232
+ # @return [Boolean]
233
+ def projection=(new_projection)
234
+ cpl_err = GDALSetProjection(@gdal_dataset, new_projection)
235
+
236
+ cpl_err.to_bool
237
+ end
238
+
239
+ # @return [Symbol]
240
+ def access_flag
241
+ return nil if null?
242
+
243
+ flag = GDALGetAccess(@gdal_dataset)
244
+
245
+ GDALAccess[flag]
246
+ end
247
+
248
+ # @return [GDAL::GeoTransform]
249
+ def geo_transform
250
+ @geo_transform ||= GeoTransform.new(@gdal_dataset)
251
+ end
252
+
253
+ # @param new_transform [GDAL::GeoTransform]
254
+ # @return [GDAL::GeoTransform]
255
+ def geo_transform=(new_transform)
256
+ new_pointer = new_transform.c_pointer.dup
257
+ cpl_err = GDALSetGeoTransform(@gdal_dataset, new_pointer)
258
+ cpl_err.to_bool
259
+
260
+ @geo_transform = GeoTransform.new(@gdal_dataset, geo_transform_pointer: new_pointer)
261
+ end
262
+
263
+ # @return [Fixnum]
264
+ def gcp_count
265
+ return 0 if null?
266
+
267
+ GDALGetGCPCount(@gdal_dataset)
268
+ end
269
+
270
+ # @return [String]
271
+ def gcp_projection
272
+ return '' if null?
273
+
274
+ GDALGetGCPProjection(@gdal_dataset)
275
+ end
276
+
277
+ # @return [FFI::GDAL::GDALGCP]
278
+ def gcps
279
+ return GDALGCP.new if null?
280
+
281
+ gcp_array_pointer = GDALGetGCPs(@gdal_dataset)
282
+
283
+ if gcp_array_pointer.null?
284
+ GDALGCP.new
285
+ else
286
+ GDALGCP.new(gcp_array_pointer)
287
+ end
288
+ end
289
+
290
+ # Iterates raster bands from 1 to #raster_count and yields them to the given
291
+ # block.
292
+ def each_band
293
+ 1.upto(raster_count) do |i|
294
+ yield(raster_band(i))
295
+ end
296
+ end
297
+
298
+ # Returns the first raster band for which the block returns true. Ex.
299
+ #
300
+ # dataset.find_band do |band|
301
+ # band.color_interpretation == :GCI_RedBand
302
+ # end
303
+ #
304
+ # @return [GDAL::RasterBand]
305
+ def find_band
306
+ each_band do |band|
307
+ result = yield(band)
308
+ return band if result
309
+ end
310
+ end
311
+
312
+ # @param red_band_array [NArray]
313
+ # @param nir_band_array [NArray]
314
+ # @return [NArray]
315
+ def calculate_ndvi(red_band_array, nir_band_array)
316
+ (nir_band_array - red_band_array) / (nir_band_array + red_band_array)
317
+ end
318
+
319
+ # @return [GDAL::RasterBand]
320
+ def red_band
321
+ find_band do |band|
322
+ band.color_interpretation == :GCI_RedBand
323
+ end
324
+ end
325
+
326
+ # @return [GDAL::RasterBand]
327
+ def green_band
328
+ find_band do |band|
329
+ band.color_interpretation == :GCI_GreenBand
330
+ end
331
+ end
332
+
333
+ # @return [GDAL::RasterBand]
334
+ def blue_band
335
+ find_band do |band|
336
+ band.color_interpretation == :GCI_BlueBand
337
+ end
338
+ end
339
+
340
+ # @return [GDAL::RasterBand]
341
+ def undefined_band
342
+ find_band do |band|
343
+ band.color_interpretation == :GCI_Undefined
344
+ end
345
+ end
346
+ end
347
+ end
@@ -0,0 +1,151 @@
1
+ require_relative '../ffi/gdal'
2
+ require_relative 'major_object'
3
+ require 'multi_xml'
4
+ require 'log_switch'
5
+
6
+
7
+ module GDAL
8
+ class Driver
9
+ include FFI::GDAL
10
+ include MajorObject
11
+ include LogSwitch::Mixin
12
+
13
+ GDAL_DOCS_URL = 'http://gdal.org'
14
+
15
+ # @return [Fixnum]
16
+ def self.driver_count
17
+ FFI::GDAL.GDALGetDriverCount
18
+ end
19
+
20
+ # @return [GDAL::Driver]
21
+ def self.by_name(name)
22
+ new(name: name)
23
+ end
24
+
25
+ # Creates a new GDAL::Driver object based on the mutually exclusive given
26
+ # parameters. Pass in only one of the allowed parameters.
27
+ #
28
+ # @param file_path [String] File to get the driver for.
29
+ # @param name [String] Name of the registered GDALDriver.
30
+ # @param index [Fixnum] Index of the registered driver. Must be less than
31
+ # GDAL::Driver.driver_count.
32
+ # @param dataset [FFI::Pointer] Pointer to the GDALDataset.
33
+ def initialize(file_path: file_path, name: name, index: index, dataset: dataset)
34
+ @gdal_driver_handle = if file_path
35
+ GDALIdentifyDriver(::File.expand_path(file_path), nil)
36
+ elsif name
37
+ GDALGetDriverByName(name)
38
+ elsif index
39
+ count = self.class.driver_count
40
+ raise "index must be between 0 and #{count - 1}." if index > count
41
+
42
+ GDALGetDriver(index)
43
+ elsif dataset
44
+ GDALGetDatasetDriver(dataset)
45
+ end
46
+ end
47
+
48
+ def c_pointer
49
+ @gdal_driver_handle
50
+ end
51
+
52
+ # @return [String]
53
+ def short_name
54
+ return '' unless @gdal_driver_handle
55
+
56
+ GDALGetDriverShortName(@gdal_driver_handle)
57
+ end
58
+
59
+ # @return [String]
60
+ def long_name
61
+ return '' unless @gdal_driver_handle
62
+
63
+ GDALGetDriverLongName(@gdal_driver_handle)
64
+ end
65
+
66
+ # @return [String]
67
+ def help_topic
68
+ return '' unless @gdal_driver_handle
69
+
70
+ "#{GDAL_DOCS_URL}/#{GDALGetDriverHelpTopic(@gdal_driver_handle)}"
71
+ end
72
+
73
+ # Lists and describes the options that can be used when calling
74
+ # GDAL::Dataset.create or GDAL::Dataset.create_copy.
75
+ #
76
+ # @return [Array]
77
+ def creation_option_list
78
+ return [] unless @gdal_driver_handle
79
+
80
+ creation_option_list_xml = GDALGetDriverCreationOptionList(@gdal_driver_handle)
81
+ MultiXml.parse(creation_option_list_xml)['CreationOptionList']['Option']
82
+ end
83
+
84
+ # Copy all of the associated files of a dataset from one file to another.
85
+ #
86
+ # @param new_name [String]
87
+ # @param old_name [String]
88
+ # @return true on success, false on warning.
89
+ # @raise [GDAL::CPLErrFailure] If failures.
90
+ def copy_dataset_files(new_name, old_name)
91
+ cpl_err = GDALCopyDatasetFiles(@gdal_driver_handle, new_name, old_name)
92
+
93
+ cpl_err.to_bool
94
+ end
95
+
96
+ # Create a new Dataset with this driver. Legal arguments depend on the
97
+ # driver and can't be retrieved programmatically.
98
+ #
99
+ # @param filename [String]
100
+ # @param x_size [Fixnum] Width of created raster in pixels.
101
+ # @param y_size [Fixnum] Height of created raster in pixels.
102
+ # @param bands [Fixnum]
103
+ # @param type [FFI::GDAL::GDALDataType]
104
+ # @return [GDAL::Dataset] Returns the *closed* dataset. You'll need to
105
+ # reopen it if you with to continue working with it.
106
+ # @todo Implement options.
107
+ def create_dataset(filename, x_size, y_size, bands: 1, type: :GDT_Byte, **options)
108
+ log "creating dataset with size #{x_size},#{y_size}"
109
+
110
+ dataset_pointer = GDALCreate(@gdal_driver_handle,
111
+ filename,
112
+ x_size,
113
+ y_size,
114
+ bands,
115
+ type,
116
+ nil
117
+ )
118
+
119
+ raise CreateFail if dataset_pointer.null?
120
+
121
+ dataset = Dataset.new(dataset_pointer)
122
+ yield(dataset) if block_given?
123
+ dataset.close
124
+
125
+ dataset
126
+ end
127
+
128
+ # Delete the dataset represented by +file_name+. Depending on the driver,
129
+ # this could mean deleting associated files, database objects, etc.
130
+ #
131
+ # @param file_name [String]
132
+ # @return true on success, false on warning.
133
+ # @raise [GDAL::CPLErrFailure] If failures.
134
+ def delete_dataset(file_name)
135
+ cpl_err = GDALDeleteDataset(@gdal_driver_handle, file_name)
136
+
137
+ cpl_err.to_bool
138
+ end
139
+
140
+ # @param new_name [String]
141
+ # @param old_name [String]
142
+ # @return true on success, false on warning.
143
+ # @raise [GDAL::CPLErrFailure] If failures.
144
+ def rename_dataset(new_name, old_name)
145
+ cpl_err = GDALRenameDataset(@gdal_driver_handle, new_name, old_name)
146
+
147
+
148
+ cpl_err.to_bool
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,17 @@
1
+ module GDAL
2
+ class OpenFailure < StandardError
3
+ def initialize(file, msg=nil)
4
+ message = msg || "Unabled to open file '#{file}'. Perhaps an unsupported file format?"
5
+ super(message)
6
+ end
7
+ end
8
+
9
+ class CPLErrFailure < StandardError
10
+ end
11
+
12
+ class CreateFail < StandardError
13
+ end
14
+
15
+ class RequiredBandNotFound < StandardError
16
+ end
17
+ end
@@ -0,0 +1,137 @@
1
+ require_relative '../ffi/gdal'
2
+
3
+
4
+ module GDAL
5
+ class GeoTransform
6
+ include FFI::GDAL
7
+
8
+ attr_accessor :gdal_dataset
9
+
10
+ # @param gdal_dataset [FFI::Pointer]
11
+ def initialize(dataset, geo_transform_pointer: nil)
12
+ @gdal_dataset = if dataset.nil?
13
+ FFI::MemoryPointer.new(:pointer)
14
+ elsif dataset.is_a? GDAL::Dataset
15
+ dataset.c_pointer
16
+ else
17
+ dataset
18
+ end
19
+
20
+ @gdal_geo_transform = if geo_transform_pointer
21
+ geo_transform_pointer
22
+ else
23
+ container_pointer = FFI::MemoryPointer.new(:double, 6)
24
+ GDALGetGeoTransform(@gdal_dataset, container_pointer).to_ruby
25
+ container_pointer
26
+ end
27
+
28
+ to_a
29
+ end
30
+
31
+ def c_pointer
32
+ @gdal_geo_transform
33
+ end
34
+
35
+ def null?
36
+ @gdal_geo_transform.null?
37
+ end
38
+
39
+ # All attributes as an Array, in the order:
40
+ # * x_origin
41
+ # * pixel_width
42
+ # * x_rotation
43
+ # * y_origin
44
+ # * y_rotation
45
+ # * pixel_height
46
+ #
47
+ # @return [Array]
48
+ def to_a
49
+ [
50
+ x_origin,
51
+ pixel_width,
52
+ x_rotation,
53
+ y_origin,
54
+ y_rotation,
55
+ pixel_height
56
+ ]
57
+ end
58
+
59
+ # X-coordinate of the center of the upper left pixel.
60
+ # In wikipedia's World Map definition, this is "C".
61
+ #
62
+ # @return [Float]
63
+ def x_origin
64
+ return nil if null?
65
+
66
+ @gdal_geo_transform[0].read_double
67
+ end
68
+
69
+ # AKA X-pixel size.
70
+ # In wikipedia's World Map definition, this is "A".
71
+ #
72
+ # @return [Float]
73
+ def pixel_width
74
+ return nil if null?
75
+
76
+ @gdal_geo_transform[1].read_double
77
+ end
78
+
79
+ # Rotation about the x-axis.
80
+ # In wikipedia's World Map definition, this is "B".
81
+ #
82
+ # @return [Float]
83
+ def x_rotation
84
+ return nil if null?
85
+
86
+ @gdal_geo_transform[2].read_double
87
+ end
88
+
89
+ # Y-coordinate of the center of the upper left pixel.
90
+ # In wikipedia's World Map definition, this is "F".
91
+ #
92
+ # @return [Float]
93
+ def y_origin
94
+ return nil if null?
95
+
96
+ @gdal_geo_transform[3].read_double
97
+ end
98
+
99
+ # Rotation about the y-axis.
100
+ # In wikipedia's World Map definition, this is "D".
101
+ #
102
+ # @return [Float]
103
+ def y_rotation
104
+ return nil if null?
105
+
106
+ @gdal_geo_transform[4].read_double
107
+ end
108
+
109
+ # AKA Y-pixel size.
110
+ # In wikipedia's World Map definition, this is "E".
111
+ #
112
+ # @return [Float]
113
+ def pixel_height
114
+ return nil if null?
115
+
116
+ @gdal_geo_transform[5].read_double
117
+ end
118
+
119
+ # The calculated UTM easting of the pixel on the map.
120
+ #
121
+ # @return [Float]
122
+ def x_projection(x_pixel, y_pixel)
123
+ return nil if null?
124
+
125
+ (pixel_width * x_pixel) + (x_rotation * y_pixel) + x_origin
126
+ end
127
+
128
+ # The calculated UTM northing of the pixel on the map.
129
+ #
130
+ # @return [Float]
131
+ def y_projection(x_pixel, y_pixel)
132
+ return nil if null?
133
+
134
+ (y_rotation * x_pixel) + (pixel_height * y_pixel) + y_origin
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,71 @@
1
+ require_relative '../ffi/gdal'
2
+
3
+
4
+ module GDAL
5
+ module MajorObject
6
+ include FFI::GDAL
7
+
8
+ # @return [Array<String>]
9
+ def metadata_domain_list
10
+ # I don't quite get it, but if #GDALGetMetadataDomainList isn't called
11
+ # twice, the last domain in the list sometimes doesn't get read.
12
+ GDALGetMetadataDomainList(c_pointer)
13
+ list_pointer = GDALGetMetadataDomainList(c_pointer)
14
+ return [] if list_pointer.null?
15
+
16
+ strings = list_pointer.get_array_of_string(0)
17
+
18
+ strings.compact.delete_if(&:empty?)
19
+ end
20
+
21
+ # @param domain [String] Name of the domain to get metadata for.
22
+ # @return [Hash]
23
+ def metadata_for_domain(domain='')
24
+ m = GDALGetMetadata(c_pointer, domain)
25
+ return {} if m.null?
26
+
27
+ data_array = m.get_array_of_string(0)
28
+
29
+ data_array.each_with_object({}) do |key_value_pair, obj|
30
+ key, value = key_value_pair.split('=', 2)
31
+
32
+ begin
33
+ obj[key] = MultiXml.parse(value)
34
+ rescue MultiXml::ParseError
35
+ obj[key] = value
36
+ end
37
+ end
38
+ end
39
+
40
+ # @param name [String]
41
+ # @param domain [String]
42
+ # @return [String]
43
+ def metadata_item(name, domain='')
44
+ GDALGetMetadataItem(c_pointer, name, domain)
45
+ end
46
+
47
+ # @return [Hash{domain => Array<String>}]
48
+ def all_metadata
49
+ sub_metadata = metadata_domain_list.each_with_object({}) do |subdomain, obj|
50
+ metadata_array = metadata_for_domain(subdomain)
51
+ obj[subdomain] = metadata_array
52
+ end
53
+
54
+ { DEFAULT: metadata_for_domain }.merge(sub_metadata)
55
+ end
56
+
57
+ # @return [String]
58
+ def description
59
+ GDALGetDescription(c_pointer)
60
+ end
61
+
62
+ # @param new_description [String]
63
+ def description=(new_description)
64
+ GDALSetDescription(c_pointer, new_description.to_s)
65
+ end
66
+
67
+ def null?
68
+ c_pointer.null?
69
+ end
70
+ end
71
+ end