diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/cluster.js | 64 | ||||
-rw-r--r-- | lib/filterOSM.js | 16 | ||||
-rw-r--r-- | lib/toOSM.js | 182 |
3 files changed, 262 insertions, 0 deletions
diff --git a/lib/cluster.js b/lib/cluster.js new file mode 100644 index 0000000..c716063 --- /dev/null +++ b/lib/cluster.js @@ -0,0 +1,64 @@ +const CheapRuler = require('cheap-ruler') +const ruler = new CheapRuler(-37, 'meters') + +/** + * Cluster points together where within threshold distance. + * + * @param {Array} features - GeoJSON Point Features + * @param {number} thresholdDistance - Maximum distance between points to cluster together + * + * @returns {Array} clusters, where unclustered features are returned as single feature clusters + */ +module.exports = (features, thresholdDistance) => { + // Array of clusters where each cluster is a Set of feature index's + const clusters = [] + + features.map((a, ai) => { + features.map((b, bi) => { + // skip comparing with self + if (ai === bi) return + + const distance = ruler.distance(a.geometry.coordinates, b.geometry.coordinates) + if (distance < thresholdDistance) { + // link into a cluster + let addedToExistingCluster = false + clusters.forEach((cluster, i) => { + if (cluster.has(ai) || cluster.has(bi)) { + // insert into this cluster + clusters[i].add(ai) + clusters[i].add(bi) + + addedToExistingCluster = true + } + }) + + if (!addedToExistingCluster) { + // create a new cluster + const newCluster = new Set() + newCluster.add(ai) + newCluster.add(bi) + clusters.push(newCluster) + } + } // else don't cluster together + }) + }) + + // result is array of clusters, including non-clustered features as single item clusters + const result = clusters.map(cluster => { + return Array.from(cluster).map(index => { + return features[index] + }) + }) + + // find features not clustered + features.map((feature, index) => { + // if feature not a cluster, return as an single item cluster + const featureInACluster = clusters.map(cluster => cluster.has(index)).reduce((acc, cur) => acc || !!cur, false) + if (!featureInACluster) { + result.push([feature]) + } + }) + + return result + +} diff --git a/lib/filterOSM.js b/lib/filterOSM.js new file mode 100644 index 0000000..e530773 --- /dev/null +++ b/lib/filterOSM.js @@ -0,0 +1,16 @@ +module.exports = (feature, options) => { + + // skip any addresses without either a housenumber or housename + // eg PFI 53396626 has no housenumber + if ( + !('addr:housenumber' in feature.properties) && + !('addr:housename' in feature.properties) + ) { + if (argv.debug) { + console.log(`PFI ${feature.properties._pfi} has neither a addr:housename or addr:housenumber, filtering`) + } + return false + } + + return true +} diff --git a/lib/toOSM.js b/lib/toOSM.js new file mode 100644 index 0000000..f9fab84 --- /dev/null +++ b/lib/toOSM.js @@ -0,0 +1,182 @@ +const { titleCase } = require('title-case') +const { capitalCase } = require('capital-case') + +const buildingUnitType = { + ANT: 'ANTENNA', + APT: 'APARTMENT', + ATM: 'ATM', + BBOX: 'BATHING BOX', + BERT: 'BERTH', + BLDG: 'BUILDING', + BTSD: 'BOATSHED', + CARP: 'CARPARK', + CARS: 'CARSPACE', + CARW: 'CARWASH', + CHAL: 'CHALET', + CLUB: 'CLUB', + CTGE: 'COTTAGE', + CTYD: 'COURTYARD', + DUPL: 'DUPLEX', + FCTY: 'FACTORY', + FLAT: 'FLAT', + GATE: 'GARAGE', + GRGE: 'GATE', + HALL: 'HALL', + HELI: 'HELIPORT', + HNGR: 'HANGAR', + HOST: 'HOSTEL', + HSE: 'HOUSE', + KSK: 'KIOSK', + LOT: 'LOT', + MBTH: 'MAISONETTE', + OFFC: 'OFFICE', + PSWY: 'PASSAGEWAY', + PTHS: 'PENTHOUSE', + REST: 'RESTAURANT', + RESV: 'RESERVE', + ROOM: 'ROOM', + RPTN: 'RECPETION', + SAPT: 'STUDIO APARTMENT', + SE: 'SUITE', + SHCS: 'SHOWCASE', + SHED: 'SHED', + SHOP: 'SHOP', + SHRM: 'SHOWROOM', + SIGN: 'SIGN', + SITE: 'SITE', + STLL: 'STALL', + STOR: 'STORE', + STR: 'STRATA UNIT', + STU: 'STUDIO', + SUBS: 'SUBSTATION', + TNCY: 'TENANCY', + TNHS: 'TOWNHOUSE', + TWR: 'TOWER', + UNIT: 'UNIT', + VLLA: 'VILLA', + VLT: 'VAULT', + WHSE: 'WAREHOUSE', + WKSH: 'WORKSHOP' +} + +// likely these are not proper names, so we will ignore them +const emptyNames = [ + 'UNNAMED', + 'NOT NAMED' +] + +/** + * Transforms a GeoJSON Feature from the Vicmap address schema into OSM schema + * + * @param sourceFeature Feature in Vicmap address schema + * @returns Feature in OSM schema + */ +module.exports = (sourceFeature, options) => { + + const outputFeature = Object.assign({}, sourceFeature) + const sourceProperties = sourceFeature.properties + const outputProperties = {} + + if (options && options.tracing) { + outputProperties['_pfi'] = sourceProperties.PFI + } + + // Building sub address type (eg UNIT OFFICE SHOP) + // + // bld_unit_* + const bld_unit_1 = [ + sourceProperties.BUNIT_PRE1, + sourceProperties.BUNIT_ID1 || null, // 0 is used for an empty value in the source data, so convert 0 to null + sourceProperties.BUNIT_SUF1 + ].join('') || null + + const bld_unit_2 = [ + sourceProperties.BUNIT_PRE2, + sourceProperties.BUNIT_ID2 || null, // 0 is used for an empty value in the source data, so convert 0 to null + sourceProperties.BUNIT_SUF2 + ].join('') || null + + // if both 1 and 2 defined, then use a range 1-2 otherwise just select the one which was defined + let bld_unit = null + if (sourceProperties.HSA_FLAG === 'Y') { + bld_unit = sourceProperties.HSAUNITID + } else { + if (bld_unit_1 && bld_unit_2) { + bld_unit = `${bld_unit_1}-${bld_unit_2}` + } else if (bld_unit_1) { + bld_unit = bld_unit_1 + } else if (bld_unit_2) { + bld_unit = bld_unit_2 + } + } + + if (bld_unit) { + outputProperties['addr:unit'] = bld_unit + } + + /* + if (sourceProperties.BLGUNTTYP && sourceProperties.BLGUNTTYP in buildingUnitType) { + outputProperties['addr:unit:type'] = buildingUnitType[sourceProperties.BLGUNTTYP] + } + */ + + if (sourceProperties.BUILDING) { + outputProperties['addr:housename'] = sourceProperties.BUILDING + } + + // house_* + const house_1 = [ + sourceProperties.HSE_PREF1, + sourceProperties.HSE_NUM1 || null, // 0 is used for an empty value in the source data, so convert 0 to null + sourceProperties.HSE_SUF1 + ].join('') + + const house_2 = [ + sourceProperties.HSE_PREF2, + sourceProperties.HSE_NUM2 || null, // 0 is used for an empty value in the source data, so convert 0 to null + sourceProperties.HSE_SUF2 + ].join('') + + let housenumber = null + if (house_1 && house_2) { + housenumber = `${house_1}-${house_2}` + } else if (house_1) { + housenumber = house_1 + } else if (house_2) { + housenumber = house_2 + } + + if (housenumber) { + outputProperties['addr:housenumber'] = housenumber + } + + // display numbers used predominately in the City of Melbourne CBD by large properties. Primarily to simplify an assigned number range. + // so should map the assigned address or the signposted address? + + // every record has at least ROAD_NAME populated + if (sourceProperties.ROAD_NAME && !emptyNames.includes(sourceProperties.ROAD_NAME)) { + outputProperties['addr:street'] = capitalCase([ + sourceProperties.ROAD_NAME, + sourceProperties.ROAD_TYPE, + sourceProperties.RD_SUF + ].join(' ')) + } + + // every record has LOCALITY populated, however some values should be empty + if (sourceProperties.LOCALITY && !emptyNames.includes(sourceProperties.LOCALITY)) { + outputProperties['addr:suburb'] = capitalCase(sourceProperties.LOCALITY) + } + + // every record has STATE populated + if (sourceProperties.STATE) { + outputProperties['addr:state'] = sourceProperties.STATE + } + + // some records have no POSTCODE populated + if (sourceProperties.POSTCODE) { + outputProperties['addr:postcode'] = sourceProperties.POSTCODE + } + + outputFeature.properties = outputProperties + return outputFeature +} |