UNPKG

9.75 kBJavaScriptView Raw
1/**
2 * @alias geojsonhint
3 * @param {(string|object)} GeoJSON given as a string or as an object
4 * @returns {Array<Object>} an array of errors
5 */
6function hint(gj) {
7
8 var errors = [];
9
10 function root(_) {
11 if (!_.type) {
12 errors.push({
13 message: 'The type property is required and was not found',
14 line: _.__line__
15 });
16 } else if (!types[_.type]) {
17 errors.push({
18 message: 'The type ' + _.type + ' is unknown',
19 line: _.__line__
20 });
21 } else if (_) {
22 types[_.type](_);
23 }
24 }
25
26 function everyIs(_, type) {
27 // make a single exception because typeof null === 'object'
28 return _.every(function(x) { return (x !== null) && (typeof x === type); });
29 }
30
31 function requiredProperty(_, name, type) {
32 if (typeof _[name] === 'undefined') {
33 return errors.push({
34 message: '"' + name + '" property required',
35 line: _.__line__
36 });
37 } else if (type === 'array') {
38 if (!Array.isArray(_[name])) {
39 return errors.push({
40 message: '"' + name +
41 '" property should be an array, but is an ' +
42 (typeof _[name]) + ' instead',
43 line: _.__line__
44 });
45 }
46 } else if (type && typeof _[name] !== type) {
47 return errors.push({
48 message: '"' + name +
49 '" property should be ' + (type) +
50 ', but is an ' + (typeof _[name]) + ' instead',
51 line: _.__line__
52 });
53 }
54 }
55
56 // http://geojson.org/geojson-spec.html#feature-collection-objects
57 function FeatureCollection(_) {
58 crs(_);
59 bbox(_);
60 if (!requiredProperty(_, 'features', 'array')) {
61 if (!everyIs(_.features, 'object')) {
62 return errors.push({
63 message: 'Every feature must be an object',
64 line: _.__line__
65 });
66 }
67 _.features.forEach(Feature);
68 }
69 }
70
71 // http://geojson.org/geojson-spec.html#positions
72 function position(_, line) {
73 if (!Array.isArray(_)) {
74 return errors.push({
75 message: 'position should be an array, is a ' + (typeof _) +
76 ' instead',
77 line: _.__line__ || line
78 });
79 } else {
80 if (_.length < 2) {
81 return errors.push({
82 message: 'position must have 2 or more elements',
83 line: _.__line__ || line
84 });
85 }
86 if (!everyIs(_, 'number')) {
87 return errors.push({
88 message: 'each element in a position must be a number',
89 line: _.__line__ || line
90 });
91 }
92 }
93 }
94
95 function positionArray(coords, type, depth, line) {
96 if (line === undefined && coords.__line__ !== undefined) {
97 line = coords.__line__;
98 }
99 if (depth === 0) {
100 return position(coords, line);
101 } else {
102 if (depth === 1 && type) {
103 if (type === 'LinearRing') {
104 if (!Array.isArray(coords[coords.length - 1])) {
105 return errors.push({
106 message: 'a number was found where a coordinate array should have been found: this needs to be nested more deeply',
107 line: line
108 });
109 }
110 if (coords.length < 4) {
111 errors.push({
112 message: 'a LinearRing of coordinates needs to have four or more positions',
113 line: line
114 });
115 }
116 if (coords.length &&
117 (coords[coords.length - 1].length !== coords[0].length ||
118 !coords[coords.length - 1].every(function(position, index) {
119 return coords[0][index] === position;
120 }))) {
121 errors.push({
122 message: 'the first and last positions in a LinearRing of coordinates must be the same',
123 line: line
124 });
125 }
126 } else if (type === 'Line' && coords.length < 2) {
127 errors.push({
128 message: 'a line needs to have two or more coordinates to be valid',
129 line: line
130 });
131 }
132 }
133 coords.forEach(function(c) {
134 positionArray(c, type, depth - 1, c.__line__ || line);
135 });
136 }
137 }
138
139 function crs(_) {
140 if (!_.crs) return;
141 if (typeof _.crs === 'object') {
142 var strErr = requiredProperty(_.crs, 'type', 'string'),
143 propErr = requiredProperty(_.crs, 'properties', 'object');
144 if (!strErr && !propErr) {
145 // http://geojson.org/geojson-spec.html#named-crs
146 if (_.crs.type === 'name') {
147 requiredProperty(_.crs.properties, 'name', 'string');
148 } else if (_.crs.type === 'link') {
149 requiredProperty(_.crs.properties, 'href', 'string');
150 }
151 }
152 } else {
153 errors.push({
154 message: 'the value of the crs property must be an object, not a ' + (typeof _.crs),
155 line: _.__line__
156 });
157 }
158 }
159
160 function bbox(_) {
161 if (!_.bbox) { return; }
162 if (Array.isArray(_.bbox)) {
163 if (!everyIs(_.bbox, 'number')) {
164 return errors.push({
165 message: 'each element in a bbox property must be a number',
166 line: _.bbox.__line__
167 });
168 }
169 } else {
170 errors.push({
171 message: 'bbox property must be an array of numbers, but is a ' + (typeof _.bbox),
172 line: _.__line__
173 });
174 }
175 }
176
177 // http://geojson.org/geojson-spec.html#point
178 function Point(_) {
179 crs(_);
180 bbox(_);
181 if (!requiredProperty(_, 'coordinates', 'array')) {
182 position(_.coordinates);
183 }
184 }
185
186 // http://geojson.org/geojson-spec.html#polygon
187 function Polygon(_) {
188 crs(_);
189 bbox(_);
190 if (!requiredProperty(_, 'coordinates', 'array')) {
191 positionArray(_.coordinates, 'LinearRing', 2);
192 }
193 }
194
195 // http://geojson.org/geojson-spec.html#multipolygon
196 function MultiPolygon(_) {
197 crs(_);
198 bbox(_);
199 if (!requiredProperty(_, 'coordinates', 'array')) {
200 positionArray(_.coordinates, 'LinearRing', 3);
201 }
202 }
203
204 // http://geojson.org/geojson-spec.html#linestring
205 function LineString(_) {
206 crs(_);
207 bbox(_);
208 if (!requiredProperty(_, 'coordinates', 'array')) {
209 positionArray(_.coordinates, 'Line', 1);
210 }
211 }
212
213 // http://geojson.org/geojson-spec.html#multilinestring
214 function MultiLineString(_) {
215 crs(_);
216 bbox(_);
217 if (!requiredProperty(_, 'coordinates', 'array')) {
218 positionArray(_.coordinates, 'Line', 2);
219 }
220 }
221
222 // http://geojson.org/geojson-spec.html#multipoint
223 function MultiPoint(_) {
224 crs(_);
225 bbox(_);
226 if (!requiredProperty(_, 'coordinates', 'array')) {
227 positionArray(_.coordinates, '', 1);
228 }
229 }
230
231 function GeometryCollection(_) {
232 crs(_);
233 bbox(_);
234 if (!requiredProperty(_, 'geometries', 'array')) {
235 _.geometries.forEach(function(geometry) {
236 if (geometry) root(geometry);
237 });
238 }
239 }
240
241 function Feature(_) {
242 crs(_);
243 bbox(_);
244 // https://github.com/geojson/draft-geojson/blob/master/middle.mkd#feature-object
245 if (_.id !== undefined &&
246 typeof _.id !== 'string' &&
247 typeof _.id !== 'number') {
248 errors.push({
249 message: 'Feature "id" property must have a string or number value',
250 line: _.__line__
251 });
252 }
253 if (_.type !== 'Feature') {
254 errors.push({
255 message: 'GeoJSON features must have a type=feature property',
256 line: _.__line__
257 });
258 }
259 requiredProperty(_, 'properties', 'object');
260 if (!requiredProperty(_, 'geometry', 'object')) {
261 // http://geojson.org/geojson-spec.html#feature-objects
262 // tolerate null geometry
263 if (_.geometry) root(_.geometry);
264 }
265 }
266
267 var types = {
268 Point: Point,
269 Feature: Feature,
270 MultiPoint: MultiPoint,
271 LineString: LineString,
272 MultiLineString: MultiLineString,
273 FeatureCollection: FeatureCollection,
274 GeometryCollection: GeometryCollection,
275 Polygon: Polygon,
276 MultiPolygon: MultiPolygon
277 };
278
279 if (typeof gj !== 'object' ||
280 gj === null ||
281 gj === undefined) {
282 errors.push({
283 message: 'The root of a GeoJSON object must be an object.',
284 line: 0
285 });
286 return errors;
287 }
288
289 root(gj);
290
291 errors.forEach(function(err) {
292 if (err.hasOwnProperty('line') && err.line === undefined) {
293 delete err.line;
294 }
295 });
296
297 return errors;
298}
299
300module.exports.hint = hint;