Skip to content

Commit 5b8a2ff

Browse files
authored
Merge pull request #43 from NetTopologySuite/fix/issue031_null_attribute_value
Allow writing shapefiles containing null attributes.
2 parents 47a3fe5 + 05b3e98 commit 5b8a2ff

File tree

11 files changed

+387
-3
lines changed

11 files changed

+387
-3
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ foreach (var feature in Shapefile.ReadAllFeatures(shpPath))
6161

6262
### Writing shapefiles using c# code
6363

64+
The most common variant of writing shapefiles is to use `Shapefile.WriteAllFeatures` method.
65+
6466
```c#
6567
var features = new List<Feature>();
6668
for (int i = 1; i < 5; i++)
@@ -89,6 +91,44 @@ for (int i = 1; i < 5; i++)
8991
Shapefile.WriteAllFeatures(features, shpPath);
9092
```
9193

94+
The most efficient way to write large shapefiles is to use `ShapefileWriter` class.
95+
This variant should also be used when you need to write a shapefile with a attributes containing `null` values.
96+
97+
```c#
98+
var fields = new List<DbfField>();
99+
var dateField = fields.AddDateField("date");
100+
var floatField = fields.AddFloatField("float");
101+
var intField = fields.AddNumericInt32Field("int");
102+
var logicalField = fields.AddLogicalField("logical");
103+
var textField = fields.AddCharacterField("text");
104+
105+
var options = new ShapefileWriterOptions(ShapeType.PolyLine, fields.ToArray());
106+
using (var shpWriter = Shapefile.OpenWrite(shpPath, options))
107+
{
108+
for (var i = 1; i < 5; i++)
109+
{
110+
var lineCoords = new List<Coordinate>
111+
{
112+
new(i, i + 1),
113+
new(i, i),
114+
new(i + 1, i)
115+
};
116+
var line = new LineString(lineCoords.ToArray());
117+
var mline = new MultiLineString(new LineString[] { line });
118+
119+
int? nullIntValue = null;
120+
121+
shpWriter.Geometry = mline;
122+
dateField.DateValue = DateTime.Now;
123+
floatField.NumericValue = i * 0.1;
124+
intField.NumericValue = nullIntValue;
125+
logicalField.LogicalValue = i % 2 == 0;
126+
textField.StringValue = i.ToString("0.00");
127+
shpWriter.Write();
128+
}
129+
}
130+
```
131+
92132
## Encoding
93133

94134
The .NET Framework supports a large number of character encodings and code pages.

src/NetTopologySuite.IO.Esri.Shapefile/Helpers/FeatureExtensions.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,78 @@ internal static DbfField[] GetDbfFields(this IAttributesTable attributes)
7474
}
7575

7676

77+
internal static DbfField[] GetDbfFields(this IEnumerable<IFeature> features)
78+
{
79+
if (features == null || !features.Any())
80+
{
81+
return null;
82+
}
83+
84+
var allAttributeNames = new HashSet<string>();
85+
var resolvedAttributeNames = new List<string>(); // preserve order of fields
86+
var attributeTypes = new Dictionary<string, Type>();
87+
88+
foreach (var feature in features)
89+
{
90+
AddAttributes(allAttributeNames, resolvedAttributeNames, attributeTypes, feature.Attributes);
91+
if (allAttributeNames.Count == resolvedAttributeNames.Count)
92+
{
93+
return GetDbfFields(resolvedAttributeNames, attributeTypes);
94+
}
95+
}
96+
97+
var missingAttributeNames = allAttributeNames.Except(resolvedAttributeNames);
98+
throw new ShapefileException("Cannot determine DBF attribut type for following attributes: " + string.Join(", ", missingAttributeNames));
99+
}
100+
101+
102+
private static void AddAttributes(HashSet<string> allAttributeNames, List<string> resolvedAttributeNames, Dictionary<string, Type> attributeTypes, IAttributesTable attributes)
103+
{
104+
if (attributes == null)
105+
{
106+
return;
107+
}
108+
109+
foreach (var attributeName in attributes.GetNames())
110+
{
111+
if (attributeTypes.ContainsKey(attributeName))
112+
{
113+
continue;
114+
}
115+
116+
allAttributeNames.Add(attributeName);
117+
118+
var attributeType = attributes.GetType(attributeName);
119+
if (attributeType != typeof(object))
120+
{
121+
resolvedAttributeNames.Add(attributeName); // preserve order of fields
122+
attributeTypes.Add(attributeName, attributeType);
123+
}
124+
}
125+
}
126+
127+
128+
private static DbfField[] GetDbfFields(List<string> attributeNames, Dictionary<string, Type> attributeTypes)
129+
{
130+
if (attributeNames.Count == 0 || attributeTypes.Count == 0)
131+
{
132+
return null;
133+
}
134+
135+
var fields = new DbfField[attributeNames.Count];
136+
137+
for (int i = 0; i < attributeNames.Count; i++)
138+
{
139+
var name = attributeNames[i];
140+
var type = attributeTypes[name];
141+
fields[i] = DbfField.Create(name, type);
142+
}
143+
144+
return fields;
145+
}
146+
147+
148+
77149
private static Geometry FindNonEmptyGeometry(Geometry geometry)
78150
{
79151
if (geometry == null || geometry.IsEmpty)

src/NetTopologySuite.IO.Esri.Shapefile/Shapefile.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,12 @@ public static void WriteAllFeatures(IEnumerable<IFeature> features, string shpPa
313313
if (features == null)
314314
throw new ArgumentNullException(nameof(features));
315315

316-
var firstFeature = features.FirstOrDefault()
317-
?? throw new ArgumentException(nameof(ShapefileWriter) + " requires at least one feature to be written.");
318-
var fields = firstFeature.Attributes.GetDbfFields();
316+
if (!features.Any())
317+
{
318+
throw new ArgumentException(nameof(ShapefileWriter) + " requires at least one feature to be written.");
319+
}
320+
321+
var fields = features.GetDbfFields();
319322
var shapeType = features.FindNonEmptyGeometry().GetShapeType();
320323
var options = new ShapefileWriterOptions(shapeType, fields)
321324
{
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using NetTopologySuite.Features;
2+
using NetTopologySuite.Geometries;
3+
using NetTopologySuite.IO.Esri.Dbf.Fields;
4+
using NetTopologySuite.IO.Esri.Shapefiles.Writers;
5+
using NUnit.Framework;
6+
using System;
7+
using System.Collections.Generic;
8+
9+
namespace NetTopologySuite.IO.Esri.Test.Issues;
10+
11+
/// <summary>
12+
/// https://github.com/NetTopologySuite/NetTopologySuite.IO.Esri/issues/31
13+
/// </summary>
14+
internal class Issue031
15+
{
16+
17+
[Test]
18+
public void CreateShp_NullInt_Auto()
19+
{
20+
var features = new List<Feature>();
21+
for (var i = 1; i < 5; i++)
22+
{
23+
var lineCoords = new List<Coordinate>
24+
{
25+
new(i, i + 1),
26+
new(i, i),
27+
new(i + 1, i)
28+
};
29+
var line = new LineString(lineCoords.ToArray());
30+
var mline = new MultiLineString(new LineString[] { line });
31+
32+
// When all features have a null value, the field type cannot be detected correctly.
33+
int? nullableIntValue = i % 3 == 0 ? 1 : null;
34+
35+
var attributes = new AttributesTable
36+
{
37+
{ "date", new DateTime() },
38+
{ "float", i * 0.1 },
39+
{ "int", nullableIntValue },
40+
{ "logical", i % 2 == 0 },
41+
{ "text", i.ToString("0.00") }
42+
};
43+
44+
45+
var feature = new Feature(mline, attributes);
46+
features.Add(feature);
47+
}
48+
49+
var shpPath = TestShapefiles.GetTempShpPath();
50+
Shapefile.WriteAllFeatures(features, shpPath);
51+
TestShapefiles.DeleteShp(shpPath);
52+
}
53+
54+
[Test]
55+
public void CreateShp_NullInt_Auto_AllNullError()
56+
{
57+
var features = new List<Feature>();
58+
for (var i = 1; i < 5; i++)
59+
{
60+
var lineCoords = new List<Coordinate>
61+
{
62+
new(i, i + 1),
63+
new(i, i),
64+
new(i + 1, i)
65+
};
66+
var line = new LineString(lineCoords.ToArray());
67+
var mline = new MultiLineString(new LineString[] { line });
68+
69+
int? nullableIntValue = null;
70+
71+
var attributes = new AttributesTable
72+
{
73+
{ "date", new DateTime() },
74+
{ "float", i * 0.1 },
75+
{ "int", nullableIntValue },
76+
{ "logical", i % 2 == 0 },
77+
{ "text", i.ToString("0.00") }
78+
};
79+
80+
81+
var feature = new Feature(mline, attributes);
82+
features.Add(feature);
83+
}
84+
85+
var shpPath = TestShapefiles.GetTempShpPath();
86+
87+
// When all features have a null value, the field type cannot be detected correctly.
88+
// To solve this, the `AttributesTable` needs to be extended to store attribute types along with attribute values,
89+
// so that `AttributesTable.GetType()` returns propert attribute type instead of default `typeof(object)`.
90+
// Se also: https://github.com/NetTopologySuite/NetTopologySuite.IO.Esri/issues/31#issuecomment-1975112219
91+
var exception = Assert.Throws<ShapefileException>(() => Shapefile.WriteAllFeatures(features, shpPath));
92+
93+
TestShapefiles.DeleteShp(shpPath);
94+
Console.WriteLine(exception.Message);
95+
}
96+
97+
[Test]
98+
public void CreateShp_NullInt_Manual_AttributeTable()
99+
{
100+
var features = new List<Feature>();
101+
for (var i = 1; i < 5; i++)
102+
{
103+
var lineCoords = new List<Coordinate>
104+
{
105+
new(i, i + 1),
106+
new(i, i),
107+
new(i + 1, i)
108+
};
109+
var line = new LineString(lineCoords.ToArray());
110+
var mline = new MultiLineString(new LineString[] { line });
111+
112+
int? nullableIntValue = null;
113+
114+
var attributes = new AttributesTable
115+
{
116+
{ "date", new DateTime() },
117+
{ "float", i * 0.1 },
118+
{ "int", nullableIntValue },
119+
{ "logical", i % 2 == 0 },
120+
{ "text", i.ToString("0.00") }
121+
};
122+
123+
var feature = new Feature(mline, attributes);
124+
features.Add(feature);
125+
}
126+
127+
var fields = new List<DbfField>();
128+
fields.AddDateField("date");
129+
fields.AddFloatField("float");
130+
fields.AddNumericInt32Field("int");
131+
fields.AddLogicalField("logical");
132+
fields.AddCharacterField("text");
133+
134+
var options = new ShapefileWriterOptions(ShapeType.PolyLine, fields.ToArray());
135+
var shpPath = TestShapefiles.GetTempShpPath();
136+
using (var shpWriter = Shapefile.OpenWrite(shpPath, options))
137+
{
138+
shpWriter.Write(features);
139+
}
140+
TestShapefiles.DeleteShp(shpPath);
141+
}
142+
143+
[Test]
144+
public void CreateShp_NullInt_Manual_ShpWriter()
145+
{
146+
var fields = new List<DbfField>();
147+
var dateField = fields.AddDateField("date");
148+
var floatField = fields.AddFloatField("float");
149+
var intField = fields.AddNumericInt32Field("int");
150+
var logicalField = fields.AddLogicalField("logical");
151+
var textField = fields.AddCharacterField("text");
152+
153+
var options = new ShapefileWriterOptions(ShapeType.PolyLine, fields.ToArray());
154+
var shpPath = TestShapefiles.GetTempShpPath();
155+
using (var shpWriter = Shapefile.OpenWrite(shpPath, options))
156+
{
157+
for (var i = 1; i < 5; i++)
158+
{
159+
var lineCoords = new List<Coordinate>
160+
{
161+
new(i, i + 1),
162+
new(i, i),
163+
new(i + 1, i)
164+
};
165+
var line = new LineString(lineCoords.ToArray());
166+
var mline = new MultiLineString(new LineString[] { line });
167+
168+
int? nullableIntValue = null;
169+
170+
shpWriter.Geometry = mline;
171+
dateField.DateValue = DateTime.Now;
172+
floatField.NumericValue = i * 0.1;
173+
intField.NumericValue = nullableIntValue;
174+
logicalField.LogicalValue = i % 2 == 0;
175+
textField.StringValue = i.ToString("0.00");
176+
shpWriter.Write();
177+
}
178+
}
179+
TestShapefiles.DeleteShp(shpPath);
180+
}
181+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using NetTopologySuite.IO.Esri.Shapefiles.Readers;
2+
using NUnit.Framework;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace NetTopologySuite.IO.Esri.Test.Issues;
10+
11+
/// <summary>
12+
/// https://github.com/NetTopologySuite/NetTopologySuite.IO.Esri/issues/33
13+
/// </summary>
14+
internal class Issue033
15+
{
16+
[Test]
17+
public void WriteAllFeatures_NullAttributeValue()
18+
{
19+
string shpPath = TestShapefiles.PathTo(@"Issues/033/sample/ne_10m_admin_0_countries_fra.shp");
20+
string shpCopyPath = TestShapefiles.GetTempShpPath();
21+
22+
var config = new ShapefileReaderOptions()
23+
{
24+
GeometryBuilderMode = GeometryBuilderMode.IgnoreInvalidShapes
25+
};
26+
27+
var features = Shapefile.ReadAllFeatures(shpPath, config);
28+
Shapefile.WriteAllFeatures(features, shpCopyPath);
29+
}
30+
}

0 commit comments

Comments
 (0)