ffi-gdal 0.0.1

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.
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