diff --git a/index.js b/index.js index e8bdd23..a410d76 100644 --- a/index.js +++ b/index.js @@ -49,10 +49,20 @@ function getArrayKey({ feature, index, geometry, props }) { } } -function unarray(arr) { +function unarray (arr) { return arr.length === 1 ? arr[0] : arr; } +// https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript +function hash (string) { + let hash = 0; + for (i = 0; i < string.length; i++) { + chr = string.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + } + return hash; +} + // assumptions // - zones is a GeoJSON with polygons // - classes are either all polygons/multi-polygons or all points (not mix of polygons and points) @@ -68,7 +78,10 @@ function calculate({ class_properties_delimiter = ",", preserve_features = true, remove_features_with_no_overlap = false, - debug_level = 0 + on_before_each_zone_feature, + on_after_each_zone_feature, + feature_filter, + debug_level = 0, }) { if (!classes) throw new Error("[zonal] classes are missing or empty"); if (!zones) throw new Error("[zonal] zones are missing or empty"); @@ -98,7 +111,6 @@ function calculate({ if ([undefined, null].includes(class_properties)) { console.warn("[zonal] you didn't pass in class_properties, so defaulting to the class feature index number"); } - // stats keyed by the unique zone+class combo id // e.g. { '["AK","Hot"]': 10, '["AK","Cold"]': 342 } @@ -113,6 +125,7 @@ function calculate({ // group class geometries into dictionary objects featureEach(classes, (class_feature, class_feature_index) => { + geomEach(class_feature, (class_geometry, class_geometry_index) => { const class_array = getArrayKey({ feature: class_feature, @@ -133,14 +146,29 @@ function calculate({ return; } - class_to_geometries[class_id] ??= []; - class_to_geometries[class_id].push(class_geometry); + const class_geometry_hash = hash(JSON.stringify(class_geometry.coordinates)); + + class_to_geometries[class_id] ??= {}; + class_to_geometries[class_id][class_geometry_hash] = class_geometry; }); }); // zones must be one or more features with polygon geometries // like administrative districts featureEach(zones, (zone_feature, zone_feature_index) => { + + if (feature_filter && feature_filter({ feature: zone_feature, index: zone_feature_index }) === false) { + return; + } + + if (typeof on_before_each_zone_feature === "function") { + on_before_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats, + zone_to_area + }); + } geomEach(zone_feature, (zone_geometry, geometry_index) => { // sometimes the same zone could be split up amonst multiple features // for example, you could have a country with multiple islands @@ -174,7 +202,7 @@ function calculate({ const combo_id = JSON.stringify([zone_id, class_id]); let remaining_zone_geometry_for_specific_class = zone_geometry; - class_geometries.forEach(class_geometry => { + Object.values(class_geometries).forEach(class_geometry => { if (class_geometry_type === "Point") { stats[combo_id] ??= { count: 0 }; @@ -226,6 +254,14 @@ function calculate({ : 0; } }); + if (typeof on_after_each_zone_feature === "function") { + on_after_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats, + zone_to_area + }); + } }); // calculate percentages diff --git a/zonal.js b/zonal.js index 5d512dd..c930073 100644 --- a/zonal.js +++ b/zonal.js @@ -86,6 +86,18 @@ function getArrayKey(_ref) { function unarray(arr) { return arr.length === 1 ? arr[0] : arr; +} // https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript + + +function hash(string) { + var hash = 0; + + for (i = 0; i < string.length; i++) { + chr = string.charCodeAt(i); + hash = (hash << 5) - hash + chr; + } + + return hash; } // assumptions // - zones is a GeoJSON with polygons // - classes are either all polygons/multi-polygons or all points (not mix of polygons and points) @@ -109,6 +121,9 @@ function calculate(_ref2) { preserve_features = _ref2$preserve_featur === void 0 ? true : _ref2$preserve_featur, _ref2$remove_features = _ref2.remove_features_with_no_overlap, remove_features_with_no_overlap = _ref2$remove_features === void 0 ? false : _ref2$remove_features, + on_before_each_zone_feature = _ref2.on_before_each_zone_feature, + on_after_each_zone_feature = _ref2.on_after_each_zone_feature, + feature_filter = _ref2.feature_filter, _ref2$debug_level = _ref2.debug_level, debug_level = _ref2$debug_level === void 0 ? 0 : _ref2$debug_level; if (!classes) throw new Error("[zonal] classes are missing or empty"); @@ -171,13 +186,30 @@ function calculate(_ref2) { return; } - (_class_to_geometries$ = class_to_geometries[class_id]) !== null && _class_to_geometries$ !== void 0 ? _class_to_geometries$ : class_to_geometries[class_id] = []; - class_to_geometries[class_id].push(class_geometry); + var class_geometry_hash = hash(JSON.stringify(class_geometry.coordinates)); + (_class_to_geometries$ = class_to_geometries[class_id]) !== null && _class_to_geometries$ !== void 0 ? _class_to_geometries$ : class_to_geometries[class_id] = {}; + class_to_geometries[class_id][class_geometry_hash] = class_geometry; }); }); // zones must be one or more features with polygon geometries // like administrative districts featureEach(zones, function (zone_feature, zone_feature_index) { + if (feature_filter && feature_filter({ + feature: zone_feature, + index: zone_feature_index + }) === false) { + return; + } + + if (typeof on_before_each_zone_feature === "function") { + on_before_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats: stats, + zone_to_area: zone_to_area + }); + } + geomEach(zone_feature, function (zone_geometry, geometry_index) { var _zone_feature$propert, _zone_to_area$zone_id; @@ -211,7 +243,7 @@ function calculate(_ref2) { // there will be a row in the table for each zone + class combo var combo_id = JSON.stringify([zone_id, class_id]); var remaining_zone_geometry_for_specific_class = zone_geometry; - class_geometries.forEach(function (class_geometry) { + Object.values(class_geometries).forEach(function (class_geometry) { if (class_geometry_type === "Point") { var _stats$combo_id; @@ -263,6 +295,15 @@ function calculate(_ref2) { stats[zone_without_class_id].area += remaining_zone_geometry_for_all_classes ? Math.round(calculateArea(remaining_zone_geometry_for_all_classes)) : 0; } }); + + if (typeof on_after_each_zone_feature === "function") { + on_after_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats: stats, + zone_to_area: zone_to_area + }); + } }); // calculate percentages entries(stats).forEach(function (_ref5) { diff --git a/zonal.min.js b/zonal.min.js index 2481826..8b16534 100644 --- a/zonal.min.js +++ b/zonal.min.js @@ -5494,6 +5494,18 @@ function getArrayKey(_ref) { function unarray(arr) { return arr.length === 1 ? arr[0] : arr; +} // https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript + + +function hash(string) { + var hash = 0; + + for (i = 0; i < string.length; i++) { + chr = string.charCodeAt(i); + hash = (hash << 5) - hash + chr; + } + + return hash; } // assumptions // - zones is a GeoJSON with polygons // - classes are either all polygons/multi-polygons or all points (not mix of polygons and points) @@ -5517,6 +5529,9 @@ function calculate(_ref2) { preserve_features = _ref2$preserve_featur === void 0 ? true : _ref2$preserve_featur, _ref2$remove_features = _ref2.remove_features_with_no_overlap, remove_features_with_no_overlap = _ref2$remove_features === void 0 ? false : _ref2$remove_features, + on_before_each_zone_feature = _ref2.on_before_each_zone_feature, + on_after_each_zone_feature = _ref2.on_after_each_zone_feature, + feature_filter = _ref2.feature_filter, _ref2$debug_level = _ref2.debug_level, debug_level = _ref2$debug_level === void 0 ? 0 : _ref2$debug_level; if (!classes) throw new Error("[zonal] classes are missing or empty"); @@ -5579,13 +5594,30 @@ function calculate(_ref2) { return; } - (_class_to_geometries$ = class_to_geometries[class_id]) !== null && _class_to_geometries$ !== void 0 ? _class_to_geometries$ : class_to_geometries[class_id] = []; - class_to_geometries[class_id].push(class_geometry); + var class_geometry_hash = hash(JSON.stringify(class_geometry.coordinates)); + (_class_to_geometries$ = class_to_geometries[class_id]) !== null && _class_to_geometries$ !== void 0 ? _class_to_geometries$ : class_to_geometries[class_id] = {}; + class_to_geometries[class_id][class_geometry_hash] = class_geometry; }); }); // zones must be one or more features with polygon geometries // like administrative districts featureEach(zones, function (zone_feature, zone_feature_index) { + if (feature_filter && feature_filter({ + feature: zone_feature, + index: zone_feature_index + }) === false) { + return; + } + + if (typeof on_before_each_zone_feature === "function") { + on_before_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats: stats, + zone_to_area: zone_to_area + }); + } + geomEach(zone_feature, function (zone_geometry, geometry_index) { var _zone_feature$propert, _zone_to_area$zone_id; @@ -5619,7 +5651,7 @@ function calculate(_ref2) { // there will be a row in the table for each zone + class combo var combo_id = JSON.stringify([zone_id, class_id]); var remaining_zone_geometry_for_specific_class = zone_geometry; - class_geometries.forEach(function (class_geometry) { + Object.values(class_geometries).forEach(function (class_geometry) { if (class_geometry_type === "Point") { var _stats$combo_id; @@ -5671,6 +5703,15 @@ function calculate(_ref2) { stats[zone_without_class_id].area += remaining_zone_geometry_for_all_classes ? Math.round(calculateArea(remaining_zone_geometry_for_all_classes)) : 0; } }); + + if (typeof on_after_each_zone_feature === "function") { + on_after_each_zone_feature({ + feature: zone_feature, + feature_index: zone_feature_index, + stats: stats, + zone_to_area: zone_to_area + }); + } }); // calculate percentages entries(stats).forEach(function (_ref5) {