aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/cluster.js64
-rw-r--r--lib/filterOSM.js16
-rw-r--r--lib/toOSM.js182
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
+}