Class: RGeo::Shapefile::Reader

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/rgeo/shapefile/reader.rb

Overview

Represents a shapefile that is open for reading.

You can use this object to read a shapefile straight through, yielding the data in a block; or you can perform random access reads of indexed records.

You must close this object after you are done, in order to close the underlying files. Alternatively, you can pass a block to Reader::open, and the reader will be closed automatically for you at the end of the block.

Dependencies

Attributes in shapefiles are stored in a “.dbf” (dBASE) format file. The “dbf” gem is required to read these files. If this gem is not installed, shapefile reading will still function, but attributes will not be available.

Correct interpretation of the polygon shape type requires some functionality that is available in the RGeo::Geos module. Hence, reading a polygon shapefile will generally fail if that module is not available or the GEOS library is not installed. It is possible to bypass this requirement by relaxing the polygon tests and making some assumptions about the file format. See the documentation for Reader::open for details.

Shapefile support

This class supports shapefiles formatted according to the 1998 “ESRI Shapefile Technical Description”. It converts shapefile data to RGeo geometry objects, as follows:

  • Shapefile records are represented by the RGeo::Shapefile::Reader::Record class, which provides the geometry, the attributes, and the record number (0-based).

  • Attribute reading is supported by the “dbf” gem, which provides the proper typecasting for numeric, string, boolean, and date/time column types. Data in unrecognized column types are returned as strings.

  • All shape types documented in the 1998 publication are supported, including point, polyline, polygon, multipoint, and multipatch, along with Z and M versions.

  • Null shapes are translated into nil geometry objects. That is, Record#geometry will return nil if that record has a null shape.

  • The point shape type yields Point geometries.

  • The multipoint shape type yields MultiPoint geometries.

  • The polyline shape type yields MultiLineString geometries.

  • The polygon shape type yields MultiPolygon geometries.

  • The multipatch shape type yields GeometryCollection geometries. (See below for an explanation of why we do not return a MultiPolygon.)

Some special notes and limitations in our shapefile support:

  • Our implementation assumes that shapefile data is in a Cartesian coordinate system when it performs certain computations, such as directionality of polygon rings. It also ignores the 180 degree longitude seam, so it may not correctly interpret objects whose coordinates are in lat/lon space and which span that seam.

  • The ESRI polygon specification allows interior rings to touch their exterior ring in a finite number of points. This technically violates the OGC Polygon definition. However, such a structure remains a legal OGC MultiPolygon, and it is in principle possible to detect this case and transform the geometry type accordingly. We do not yet do this. Therefore, it is possible for a shapefile with polygon type to yield an illegal geometry.

  • The ESRI polygon specification clearly specifies the winding order for inner and outer rings: outer rings are clockwise while inner rings are counterclockwise. We have heard it reported that there may be shapefiles out there that do not conform to this spec. Such shapefiles may not read correctly.

  • The ESRI multipatch specification includes triangle strips and triangle fans as ways of constructing polygonal patches. We read in the aggregate polygonal patches, and do not preserve the individual triangles.

  • The ESRI multipatch specification allows separate patch parts to share common boundaries, thus effectively becoming a single polygon. It is in principle possible to detect this case and merge the constituent polygons; however, such a data structure implies that the intent is for such polygons to remain distinct objects even though they share a common boundary. Therefore, we do not attempt to merge such polygons. However, this means it is possible for a multipatch to violate the OGC MultiPolygon assertions, which do not allow constituent polygons to share a common boundary. Therefore, when reading a multipatch, we return a GeometryCollection instead of a MultiPolygon.

Defined Under Namespace

Classes: Record

Constant Summary collapse

NODATA_LIMIT =

Values less than this value are considered “no value” in the shapefile format specification.

-1e38

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_, opts_ = {}) ⇒ Reader

Low-level creation of a Reader. The arguments are the same as those passed to Reader::open, except that this doesn’t take a block. You should use Reader::open instead.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/rgeo/shapefile/reader.rb', line 178

def initialize(path_, opts_ = {}) # :nodoc:
  @allow_unsafe = false
  if opts_[:allow_unsafe]
    if Gem::Version.new(RGeo::VERSION) < Gem::Version.new("3.0.0")
      warn "RGeo v#{RGeo::VERSION} does not support unsafe methods. Unsafe methods were added in RGeo v3.0.0."
    else
      @allow_unsafe = opts_[:allow_unsafe]
    end
  end
  path_ = path_.to_s.sub(/\.shp$/, "")
  @base_path = path_
  @opened = true
  @main_file = ::File.open("#{path_}.shp", "rb:ascii-8bit")
  @index_file = ::File.open("#{path_}.shx", "rb:ascii-8bit")
  @attr_dbf =
    if ::File.file?("#{path_}.dbf") && ::File.readable?("#{path_}.dbf")
      if ::File.file?("#{path_}.cpg") && ::File.readable?("#{path_}.cpg")
        dbf_encoding_ = ::File.read("#{path_}.cpg")
        dbf_encoding_ = begin
          Encoding.find(dbf_encoding_.to_s.strip)
        rescue StandardError
          nil
        end
      end

      ::DBF::Table.new("#{path_}.dbf", nil, dbf_encoding_)
    end
  @main_length, @shape_type_code, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin, @mmax = @main_file.read(100).unpack("x24Nx4VE8")
  @main_length *= 2
  index_length_ = @index_file.read(100).unpack("x24Nx72").first
  @num_records = (index_length_ - 50) / 4
  @cur_record_index = 0

  if @num_records == 0
    @xmin = @xmax = @ymin = @ymax = @zmin = @zmax = @mmin = @mmax = nil
  else
    case @shape_type_code
    when 11, 13, 15, 18, 31
      if @mmin < NODATA_LIMIT || @mmax < NODATA_LIMIT
        @mmin = @mmax = nil
      end
      if @zmin < NODATA_LIMIT || @zmax < NODATA_LIMIT
        @zmin = @zmax = nil
      end
    when 21, 23, 25, 28
      @zmin = @zmax = nil
    else
      @mmin = @mmax = @zmin = @zmax = nil
    end
  end

  @factory = opts_[:factory_generator] || opts_[:factory] || Cartesian.method(:preferred_factory)
  unless @factory.is_a?(Feature::Factory::Instance)
    factory_config_ = {}
    factory_config_[:srid] = opts_[:srid] if opts_[:srid]
    unless @zmin.nil?
      factory_config_[:has_z_coordinate] = true
    end
    unless @mmin.nil?
      factory_config_[:has_m_coordinate] = true
    end
    @factory = @factory.call(factory_config_)
  end
  @factory_supports_z = @factory.property(:has_z_coordinate)
  @factory_supports_m = @factory.property(:has_m_coordinate)

  @assume_inner_follows_outer = opts_[:assume_inner_follows_outer]
end

Class Method Details

.open(path_, opts_ = {}, &block_) ⇒ Object

Create a new shapefile reader. You must pass the path for the main shapefile (e.g. “path/to/file.shp”). You may also omit the “.shp” extension from the path. All three files that make up the shapefile (“.shp”, “.shx”, and “.dbf”) must be present for successful opening of a shapefile.

You must also provide a RGeo::Feature::FactoryGenerator. It should understand the configuration options :has_z_coordinate and :has_m_coordinate. You may also pass a specific RGeo::Feature::Factory, or nil to specify the default Cartesian FactoryGenerator.

If you provide a block, the shapefile reader will be yielded to the block, and automatically closed at the end of the block. In this instance, File.open returns the value of the block. If you do not provide a block, the shapefile reader will be returned from this call. It is then the caller’s responsibility to close the reader when it is done.

Options include:

:factory_generator

A RGeo::Feature::FactoryGenerator that should return a factory based on the dimension settings in the input. It should understand the configuration options :has_z_coordinate and :has_m_coordinate. You may also pass a specific RGeo::Feature::Factory. If no factory generator is provided, the default Cartesian factory generator is used. This option can also be specified using the :factory key.

:srid

If provided, this option is passed to the factory generator. This is useful because shapefiles do not contain a SRID.

:assume_inner_follows_outer

If set to true, some assumptions are made about ring ordering in a polygon shapefile. See below for details. Default is false.

Ring ordering in polygon shapefiles

The ESRI polygon shape type specifies that the ordering of rings in the shapefile is not significant. That is, rings can be in any order, and inner rings need not necessarily follow the outer ring they are associated with. This specification causes some headache in the process of constructing polygons from a shapefile, because it becomes necessary to run some geometric analysis on the rings that are read in, in order to determine which inner rings should go with which outer rings.

RGeo’s shapefile reader uses GEOS to perform this analysis. However, this means that if GEOS is not available, the analysis will fail. It also means reading polygons may be slow, especially for polygon records with a large number of parts. Therefore, it is possible to turn off this analysis by setting the :assume_inner_follows_outer switch when creating a Reader. This causes the shapefile reader to assume that inner rings always follow their corresponding outer ring in the file. This is probably true for most well-behaved shapefiles out there, but since it is not part of the specification, this shortcutting is not turned on by default. However, if you are running RGeo on a platform without GEOS, you have no choice but to turn on this switch and make this assumption about your input shapefiles.



161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/rgeo/shapefile/reader.rb', line 161

def self.open(path_, opts_ = {}, &block_)
  file_ = new(path_, opts_)
  if block_
    begin
      yield file_
    ensure
      file_.close
    end
  else
    file_
  end
end

Instance Method Details

#_read_multipatch(data_) ⇒ Object

:nodoc:



676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
# File 'lib/rgeo/shapefile/reader.rb', line 676

def _read_multipatch(data_) # :nodoc:
  # Read counts
  num_parts_, num_points_ = data_[36, 8].unpack("VV")

  # Read remaining data
  values_ = data_[44, 32 + num_parts_ * 8 + num_points_ * 32].unpack("V#{num_parts_ * 2}E*")

  # Parts arrays
  part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]
  part_types_ = values_.slice!(0, num_parts_)

  # Extract XY, Z, and M values
  xys_ = values_.slice!(0, num_points_ * 2)
  zs_ = values_.slice!(2, num_points_)
  zs_.map! { |val_| val_ < NODATA_LIMIT ? 0 : val_ } if zs_
  ms_ = values_.slice!(4, num_points_)
  ms_.map! { |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_

  # Generate points
  points_ = (0..num_points_ - 1).map do |i_|
    extras_ = []
    extras_ << zs_[i_] if zs_ && @factory_supports_z
    extras_ << ms_[i_] if ms_ && @factory_supports_m
    @factory.point(xys_[i_ * 2], xys_[i_ * 2 + 1], *extras_)
  end

  # Create the parts
  parts_ = (0..num_parts_ - 1).map do |i_|
    ps_ = points_[part_indexes_[i_]...part_indexes_[i_ + 1]]
    # All part types just translate directly into rings, except for
    # triangle fan, which requires that we reorder the vertices.
    if part_types_[i_] == 0
      ps2_ = []
      i2_ = 0
      while i2_ < ps_.size
        ps2_ << ps_[i2_]
        i2_ += 2
      end
      i2_ -= 1
      i2_ -= 2 if i2_ >= ps_.size
      while i2_ > 0
        ps2_ << ps_[i2_]
        i2_ -= 2
      end
      ps_ = ps2_
    end
    @factory.linear_ring(ps_)
  end

  # Get a GEOS factory if needed.
  geos_factory_ = nil
  unless @assume_inner_follows_outer
    geos_factory_ = Geos.factory
    unless geos_factory_
      raise Error::RGeoError, "GEOS is not available, but is required for correct interpretation of polygons in shapefiles."
    end
  end

  # Walk the parts and generate polygons
  polygons_ = []
  state_ = :empty
  sequence_ = []
  # We deliberately include num_parts_ so there's an extra iteration
  # with a null part_ and type_. This is so the state handling block
  # can finish up any currently live sequence.
  (0..num_parts_).each do |index_|
    part_ = parts_[index_]
    type_ = part_types_[index_]

    # This section handles any state.
    # It either stays in the state and goes to the next part,
    # or it wraps up the state. Either way, at the end of this
    # case block, the state must be :empty.
    case state_
    when :outer
      if type_ == 3
        # Inner ring in an outer-led sequence.
        # Just add it to the sequence and continue.
        sequence_ << part_
        next
      else
        # End of an outer-led sequence.
        # Add the polygon and reset the state.
        polygons_ << @factory.polygon(sequence_[0], sequence_[1..])
        state_ = :empty
        sequence_ = []
      end
    when :first
      if type_ == 5
        # Unknown ring in a first-led sequence.
        # Just add it to the sequence and continue.
        sequence_ << part_
      else
        # End of a first-led sequence.
        # Need to determine which is the outer ring before we can
        # add the polygon.
        # If :assume_inner_follows_outer is in effect, we assume
        # the first ring is the outer one. Otherwise, we have to
        # use GEOS to determine containment.
        unless @assume_inner_follows_outer
          geos_polygons_ = sequence_.map { |ring_| geos_factory_.polygon(ring_) }
          outer_poly_ = nil
          outer_index_ = 0
          contains_method_ = @allow_unsafe ? :unsafe_contains? : :contains?
          geos_polygons_.each_with_index do |poly_, idx_|
            if outer_poly_
              if poly_.public_send(contains_method_, outer_poly_)
                outer_poly_ = poly_
                outer_index_ = idx_
                break
              end
            else
              outer_poly_ = poly_
            end
          end
          sequence_.slice!(outer_index_)
          sequence_.unshift(outer_poly_)
        end
        polygons_ << @factory.polygon(sequence_[0], sequence_[1..])
        state_ = :empty
        sequence_ = []
      end
    end

    # State is now :empty. We allow any type except 3 (since an
    # (inner must come during an outer-led sequence).
    # We treat a type 5 ring that isn't part of a first-led sequence
    # as an outer ring.
    case type_
    when 0, 1
      polygons_ << @factory.polygon(part_)
    when 2, 5
      sequence_ << part_
      state_ = :outer
    when 4
      sequence_ << part_
      state_ = :first
    end
  end

  # Return the geometry as a collection.
  @factory.collection(polygons_)
end

#_read_multipoint(data_, opt_ = nil) ⇒ Object

:nodoc:



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/rgeo/shapefile/reader.rb', line 455

def _read_multipoint(data_, opt_ = nil) # :nodoc:
  # Read number of points
  num_points_ = data_[36, 4].unpack("V").first

  # Read remaining data
  size_ = num_points_ * 16
  size_ += 16 + num_points_ * 8 if opt_
  size_ += 16 + num_points_ * 8 if opt_ == :z
  values_ = data_[40, size_].unpack("E*")

  # Extract XY, Z, and M values
  xys_ = values_.slice!(0, num_points_ * 2)
  ms_ = nil
  zs_ = nil
  if opt_
    ms_ = values_.slice!(2, num_points_)
    if opt_ == :z
      zs_ = ms_
      ms_ = values_.slice!(4, num_points_)
      ms_.map! { |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
    end
  end

  # Generate points
  points_ = (0..num_points_ - 1).map do |i_|
    extras_ = []
    extras_ << zs_[i_] if zs_ && @factory_supports_z
    extras_ << ms_[i_] if ms_ && @factory_supports_m
    @factory.point(xys_[i_ * 2], xys_[i_ * 2 + 1], *extras_)
  end

  # Return a MultiPoint
  @factory.multi_point(points_)
end

#_read_next_recordObject

:nodoc:



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/rgeo/shapefile/reader.rb', line 404

def _read_next_record # :nodoc:
  length_ = @main_file.read(8).unpack("NN")[1]
  data_ = @main_file.read(length_ * 2)
  shape_type_ = data_[0, 4].unpack("V").first
  geometry_ =
    case shape_type_
    when 1 then _read_point(data_)
    when 3 then _read_polyline(data_)
    when 5 then _read_polygon(data_)
    when 8 then _read_multipoint(data_)
    when 11 then _read_point(data_, :z)
    when 13 then _read_polyline(data_, :z)
    when 15 then _read_polygon(data_, :z)
    when 18 then _read_multipoint(data_, :z)
    when 21 then _read_point(data_, :m)
    when 23 then _read_polyline(data_, :m)
    when 25 then _read_polygon(data_, :m)
    when 28 then _read_multipoint(data_, :m)
    when 31 then _read_multipatch(data_)
    end
  attrs_ = {}
  if @attr_dbf && !@attr_dbf.columns.empty? && @attr_dbf.record(@cur_record_index)
    dbf_record_attrs_ = @attr_dbf.record(@cur_record_index).attributes
    @attr_dbf.columns.each do |col_|
      name_ = col_.name
      attrs_[name_] = dbf_record_attrs_[name_]
    end
  end
  result_ = Record.new(@cur_record_index, geometry_, attrs_)
  @cur_record_index += 1
  result_
end

#_read_point(data_, opt_ = nil) ⇒ Object

:nodoc:



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/rgeo/shapefile/reader.rb', line 437

def _read_point(data_, opt_ = nil) # :nodoc:
  case opt_
  when :z
    x_, y_, z_, m_ = data_[4, 32].unpack("EEEE")
    m_ = 0 if m_.nil? || m_ < NODATA_LIMIT
  when :m
    x_, y_, m_ = data_[4, 24].unpack("EEE")
    z_ = 0
  else
    x_, y_ = data_[4, 16].unpack("EE")
    z_ = m_ = 0
  end
  extras_ = []
  extras_ << z_ if @factory_supports_z
  extras_ << m_ if @factory_supports_m
  @factory.point(x_, y_, *extras_)
end

#_read_polygon(data_, opt_ = nil) ⇒ Object

:nodoc:



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'lib/rgeo/shapefile/reader.rb', line 533

def _read_polygon(data_, opt_ = nil) # :nodoc:
  # Read counts
  num_parts_, num_points_ = data_[36, 8].unpack("VV")

  # Read remaining data
  size_ = num_parts_ * 4 + num_points_ * 16
  size_ += 16 + num_points_ * 8 if opt_
  size_ += 16 + num_points_ * 8 if opt_ == :z
  values_ = data_[44, size_].unpack("V#{num_parts_}E*")

  # Parts array
  part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]

  # Extract XY, Z, and M values
  xys_ = values_.slice!(0, num_points_ * 2)
  ms_ = nil
  zs_ = nil
  if opt_
    ms_ = values_.slice!(2, num_points_)
    if opt_ == :z
      zs_ = ms_
      ms_ = values_.slice!(4, num_points_)
      ms_.map! { |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
    end
  end

  # Generate points
  points_ = (0..num_points_ - 1).map do |i_|
    extras_ = []
    extras_ << zs_[i_] if zs_ && @factory_supports_z
    extras_ << ms_[i_] if ms_ && @factory_supports_m
    @factory.point(xys_[i_ * 2], xys_[i_ * 2 + 1], *extras_)
  end.compact

  # The parts are LinearRing objects
  parts_ = (0..num_parts_ - 1).map do |i_|
    @factory.linear_ring(points_[part_indexes_[i_]...part_indexes_[i_ + 1]])
  end.compact

  # Get a GEOS factory if needed.
  geos_factory_ = nil
  unless @assume_inner_follows_outer
    geos_factory_ = Geos.factory
    unless geos_factory_
      raise Error::RGeoError, "GEOS is not available, but is required for correct interpretation of polygons in shapefiles."
    end
  end

  # Special case: if there's only one part, treat it as an outer
  # ring, regardless of its direction. This isn't strictly compliant
  # with the shapefile spec, but the shapelib test cases seem to
  # include this case, so we'll relax the assertions here.
  if parts_.size == 1
    return @factory.multi_polygon([@factory.polygon(parts_[0])])
  end

  # Collect some data on the rings: the ring direction, a GEOS
  # polygon (for intersection calculation), and an initial guess
  # of which polygon index the ring belongs to.
  parts_.map! do |ring_|
    [ring_, Cartesian::Analysis.ring_direction(ring_) < 0, geos_factory_ ? geos_factory_.polygon(ring_) : nil, nil]
  end

  # Initial population of the polygon data array.
  # Each element is an array of the part data for the rings, first
  # the outer ring and then the inner rings.
  # Here we populate the outer rings, and we do an initial
  # assignment of rings to polygon index. The initial guess is that
  # inner rings always follow their outer ring.
  polygons_ = []
  parts_.each do |part_data_|
    if part_data_[1]
      polygons_ << [part_data_]
    elsif @assume_inner_follows_outer && polygons_.size > 0
      polygons_.last << part_data_
    end
    part_data_[3] = polygons_.size - 1
  end

  # If :assume_inner_follows_outer is in effect, we assume this
  # initial guess is the correct one, and we don't run the
  # potentially expensive intersection tests.
  unless @assume_inner_follows_outer
    case polygons_.size
    when 0
      # Skip this algorithm if there's no outer
    when 1
      # Shortcut if there's only one outer. Assume all the inners
      # are members of this one polygon.
      parts_.each do |part_data_|
        unless part_data_[1]
          polygons_[0] << part_data_
        end
      end
    else
      # Go through the remaining (inner) rings, and assign them to
      # the correct polygon. For each inner ring, we find the outer
      # ring containing it, and add it to that polygon's data. We
      # check the initial guess first, and if it fails we go through
      # the remaining polygons in order.
      parts_.each do |part_data_|
        unless part_data_[1]
          # This will hold the polygon index for this inner ring.
          parent_index_ = nil
          # The initial guess. It could be -1 if this inner ring
          # appeared before any outer rings had appeared.
          first_try_ = part_data_[3]

          within_method_ = @allow_unsafe ? :unsafe_within? : :within?
          if first_try_ >= 0 && part_data_[2].public_send(within_method_, polygons_[first_try_].first[2])
            parent_index_ = first_try_
          end
          # If the initial guess didn't work, go through the
          # remaining polygons and check their outer rings.
          unless parent_index_
            polygons_.each_with_index do |poly_data_, index_|
              if index_ != first_try_ && part_data_[2].public_send(within_method_, poly_data_.first[2])
                parent_index_ = index_
                break
              end
            end
          end
          # If we found a match, append this inner ring to that
          # polygon data. Otherwise, just throw away the inner ring.
          if parent_index_
            polygons_[parent_index_] << part_data_
          end
        end
      end
    end
  end

  # Generate the actual polygons from the collected polygon data
  polygons_.map! do |poly_data_|
    outer_ = poly_data_[0][0]
    inner_ = poly_data_[1..].map { |part_data_| part_data_[0] }
    @factory.polygon(outer_, inner_)
  end

  # Finally, return the MultiPolygon.
  @factory.multi_polygon(polygons_)
end

#_read_polyline(data_, opt_ = nil) ⇒ Object

:nodoc:



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/rgeo/shapefile/reader.rb', line 490

def _read_polyline(data_, opt_ = nil) # :nodoc:
  # Read counts
  num_parts_, num_points_ = data_[36, 8].unpack("VV")

  # Read remaining data
  size_ = num_parts_ * 4 + num_points_ * 16
  size_ += 16 + num_points_ * 8 if opt_
  size_ += 16 + num_points_ * 8 if opt_ == :z
  values_ = data_[44, size_].unpack("V#{num_parts_}E*")

  # Parts array
  part_indexes_ = values_.slice!(0, num_parts_) + [num_points_]

  # Extract XY, Z, and M values
  xys_ = values_.slice!(0, num_points_ * 2)
  ms_ = nil
  zs_ = nil
  if opt_
    ms_ = values_.slice!(2, num_points_)
    if opt_ == :z
      zs_ = ms_
      ms_ = values_.slice!(4, num_points_)
      ms_.map! { |val_| val_ < NODATA_LIMIT ? 0 : val_ } if ms_
    end
  end

  # Generate points
  points_ = (0..num_points_ - 1).map do |i_|
    extras_ = []
    extras_ << zs_[i_] if zs_ && @factory_supports_z
    extras_ << ms_[i_] if ms_ && @factory_supports_m
    @factory.point(xys_[i_ * 2], xys_[i_ * 2 + 1], *extras_)
  end

  # Generate LineString objects (parts)
  parts_ = (0..num_parts_ - 1).map do |i_|
    @factory.line_string(points_[part_indexes_[i_]...part_indexes_[i_ + 1]])
  end

  # Generate MultiLineString
  @factory.multi_line_string(parts_)
end

#attributes_available?Boolean

Returns true if attributes are available. This may be false because there is no “.dbf” file or because the dbf gem is not available.

Returns:

  • (Boolean)


270
271
272
# File 'lib/rgeo/shapefile/reader.rb', line 270

def attributes_available?
  @opened ? (@attr_dbf ? true : false) : nil
end

#closeObject

Close the shapefile. You should not use this Reader after it has been closed. Most methods will return nil.



251
252
253
254
255
256
257
# File 'lib/rgeo/shapefile/reader.rb', line 251

def close
  return unless @opened
  @main_file.close
  @index_file.close
  @attr_dbf.close if @attr_dbf
  @opened = false
end

#cur_indexObject

Returns the current file pointer as a record index (0-based). This is the record number that will be read when Reader#next is called.



346
347
348
# File 'lib/rgeo/shapefile/reader.rb', line 346

def cur_index
  @opened ? @cur_record_index : nil
end

#eachObject

Read the remaining records starting with the current record index, and yield the Reader::Record for each one.

Raises:

  • (IOError)


359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/rgeo/shapefile/reader.rb', line 359

def each
  return to_enum(:each) { @num_records } unless block_given?
  raise IOError, "File was not open" unless @opened
  # Each needs to be idempotent, therefore we reset all the internal indexes to their original value
  current_record_index = @cur_record_index
  begin
    rewind
    yield _read_next_record while @cur_record_index < @num_records
  ensure
    seek_index(current_record_index)
  end
  self
end

#factoryObject

Returns the factory used by this reader.



276
277
278
# File 'lib/rgeo/shapefile/reader.rb', line 276

def factory
  @opened ? @factory : nil
end

#get(index_) ⇒ Object Also known as: []

Get the given record number. Equivalent to seeking to that index and calling next.



399
400
401
# File 'lib/rgeo/shapefile/reader.rb', line 399

def get(index_)
  seek_index(index_) ? self.next : nil
end

#mmaxObject

Returns the maximum m, or nil if the shapefile does not contain m.



338
339
340
# File 'lib/rgeo/shapefile/reader.rb', line 338

def mmax
  @opened ? @mmax : nil
end

#mminObject

Returns the minimum m, or nil if the shapefile does not contain m.



332
333
334
# File 'lib/rgeo/shapefile/reader.rb', line 332

def mmin
  @opened ? @mmin : nil
end

#nextObject

Read and return the next record as a Reader::Record.



352
353
354
# File 'lib/rgeo/shapefile/reader.rb', line 352

def next
  @opened && @cur_record_index < @num_records ? _read_next_record : nil
end

#num_recordsObject Also known as: size

Returns the number of records in the shapefile.



282
283
284
# File 'lib/rgeo/shapefile/reader.rb', line 282

def num_records
  @opened ? @num_records : nil
end

#open?Boolean

Returns true if this Reader is still open, or false if it has been closed.

Returns:

  • (Boolean)


262
263
264
# File 'lib/rgeo/shapefile/reader.rb', line 262

def open?
  @opened
end

#rewindObject

Rewind to the beginning of the file. Equivalent to seek_index(0).



392
393
394
# File 'lib/rgeo/shapefile/reader.rb', line 392

def rewind
  seek_index(0)
end

#seek_index(index_) ⇒ Object

Seek to the given record index.



375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/rgeo/shapefile/reader.rb', line 375

def seek_index(index_)
  if @opened && index_ >= 0 && index_ <= @num_records
    if index_ < @num_records && index_ != @cur_record_index
      @index_file.seek(100 + 8 * index_)
      offset_ = @index_file.read(4).unpack("N").first
      @main_file.seek(offset_ * 2)
    end
    @cur_record_index = index_
    true
  else
    false
  end
end

#shape_type_codeObject

Returns the shape type code.



290
291
292
# File 'lib/rgeo/shapefile/reader.rb', line 290

def shape_type_code
  @opened ? @shape_type_code : nil
end

#xmaxObject

Returns the maximum x.



302
303
304
# File 'lib/rgeo/shapefile/reader.rb', line 302

def xmax
  @opened ? @xmax : nil
end

#xminObject

Returns the minimum x.



296
297
298
# File 'lib/rgeo/shapefile/reader.rb', line 296

def xmin
  @opened ? @xmin : nil
end

#ymaxObject

Returns the maximum y.



314
315
316
# File 'lib/rgeo/shapefile/reader.rb', line 314

def ymax
  @opened ? @ymax : nil
end

#yminObject

Returns the minimum y.



308
309
310
# File 'lib/rgeo/shapefile/reader.rb', line 308

def ymin
  @opened ? @ymin : nil
end

#zmaxObject

Returns the maximum z, or nil if the shapefile does not contain z.



326
327
328
# File 'lib/rgeo/shapefile/reader.rb', line 326

def zmax
  @opened ? @zmax : nil
end

#zminObject

Returns the minimum z, or nil if the shapefile does not contain z.



320
321
322
# File 'lib/rgeo/shapefile/reader.rb', line 320

def zmin
  @opened ? @zmin : nil
end