Highlight possibly-conflicting (nearby) addresses in yellow
This commit is contained in:
@@ -27,6 +27,9 @@ import warnings
|
|||||||
warnings.filterwarnings('ignore')
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
|
||||||
|
CONFLICT_RADIUS_METERS = 5.0
|
||||||
|
|
||||||
|
|
||||||
class AddressComparator:
|
class AddressComparator:
|
||||||
def __init__(self, tolerance_meters: float = 500.0):
|
def __init__(self, tolerance_meters: float = 500.0):
|
||||||
self.tolerance_meters = tolerance_meters
|
self.tolerance_meters = tolerance_meters
|
||||||
@@ -62,6 +65,27 @@ class AddressComparator:
|
|||||||
s = re.sub(r'\s+', ' ', s).strip()
|
s = re.sub(r'\s+', ' ', s).strip()
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _flag_close_conflicts(addresses: List[Dict], radius_meters: float = CONFLICT_RADIUS_METERS) -> None:
|
||||||
|
"""Mark each address dict's 'conflict' key True when another point in the
|
||||||
|
same list falls within radius_meters. Doesn't dedupe or exclude anything -
|
||||||
|
just surfaces tight clusters (e.g. ambiguous source data reusing the same
|
||||||
|
unit label across distinct points) for manual review in the map UI."""
|
||||||
|
if not addresses:
|
||||||
|
return
|
||||||
|
geoms = [a['geometry'] for a in addresses]
|
||||||
|
tree = STRtree(geoms)
|
||||||
|
radius_deg = radius_meters / 111000.0
|
||||||
|
for i, addr in enumerate(addresses):
|
||||||
|
conflict = False
|
||||||
|
for j in tree.query(geoms[i].buffer(radius_deg)):
|
||||||
|
if j == i:
|
||||||
|
continue
|
||||||
|
if geoms[i].distance(geoms[j]) * 111000.0 <= radius_meters:
|
||||||
|
conflict = True
|
||||||
|
break
|
||||||
|
addr['conflict'] = conflict
|
||||||
|
|
||||||
def compare_addresses(self, local_file: str, osm_file: str) -> Tuple[List[Dict], List[Dict], List[Dict]]:
|
def compare_addresses(self, local_file: str, osm_file: str) -> Tuple[List[Dict], List[Dict], List[Dict]]:
|
||||||
"""Compare local and OSM address data.
|
"""Compare local and OSM address data.
|
||||||
|
|
||||||
@@ -175,6 +199,8 @@ class AddressComparator:
|
|||||||
props['status'] = 'removed'
|
props['status'] = 'removed'
|
||||||
removed_addresses.append({'geometry': osm_row.geometry, **props})
|
removed_addresses.append({'geometry': osm_row.geometry, **props})
|
||||||
|
|
||||||
|
self._flag_close_conflicts(new_addresses)
|
||||||
|
|
||||||
return new_addresses, existing_addresses, removed_addresses
|
return new_addresses, existing_addresses, removed_addresses
|
||||||
|
|
||||||
def save_results(self, new_addresses, existing_addresses, removed_addresses, output_dir):
|
def save_results(self, new_addresses, existing_addresses, removed_addresses, output_dir):
|
||||||
|
|||||||
@@ -160,12 +160,6 @@ def convert(zip_path, output_path):
|
|||||||
zip_path = Path(zip_path)
|
zip_path = Path(zip_path)
|
||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
|
|
||||||
# Skip if output is newer than input
|
|
||||||
if (output_path.exists() and zip_path.exists() and
|
|
||||||
output_path.stat().st_mtime > zip_path.stat().st_mtime):
|
|
||||||
print(f"Output is up to date: {output_path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Converting {zip_path} ...")
|
print(f"Converting {zip_path} ...")
|
||||||
|
|
||||||
exceptions = load_exceptions()
|
exceptions = load_exceptions()
|
||||||
|
|||||||
+50
-13
@@ -312,8 +312,10 @@ function diffStyle(feature) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True');
|
const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True');
|
||||||
|
const isConflicted = !isRemoved && feature.properties &&
|
||||||
|
(feature.properties.conflict === true || feature.properties.conflict === 'True');
|
||||||
return {
|
return {
|
||||||
color: isRemoved ? '#ff0000' : '#00ff00',
|
color: isRemoved ? '#ff0000' : (isConflicted ? '#c9a800' : '#00ff00'),
|
||||||
weight: 3,
|
weight: 3,
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
};
|
};
|
||||||
@@ -344,10 +346,12 @@ function diffMarkerStyle(feature) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True');
|
const isRemoved = feature.properties && (feature.properties.removed === true || feature.properties.removed === 'True');
|
||||||
|
const isConflicted = !isRemoved && feature.properties &&
|
||||||
|
(feature.properties.conflict === true || feature.properties.conflict === 'True');
|
||||||
return {
|
return {
|
||||||
radius: 6,
|
radius: 6,
|
||||||
fillColor: isRemoved ? '#ff0000' : '#00ff00',
|
fillColor: isRemoved ? '#ff0000' : (isConflicted ? '#ffeb3b' : '#00ff00'),
|
||||||
color: isRemoved ? '#cc0000' : '#00cc00',
|
color: isRemoved ? '#cc0000' : (isConflicted ? '#c9a800' : '#00cc00'),
|
||||||
weight: 1,
|
weight: 1,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
fillOpacity: 0.7
|
fillOpacity: 0.7
|
||||||
@@ -457,17 +461,20 @@ function shouldShowFeature(feature) {
|
|||||||
if (isExcluded(feature)) return false;
|
if (isExcluded(feature)) return false;
|
||||||
const props = feature.properties || {};
|
const props = feature.properties || {};
|
||||||
const isRemoved = props.removed === true || props.removed === 'True';
|
const isRemoved = props.removed === true || props.removed === 'True';
|
||||||
|
const isConflicted = !isRemoved && (props.conflict === true || props.conflict === 'True');
|
||||||
const isService = props.highway === 'service' || props.highway === 'track';
|
const isService = props.highway === 'service' || props.highway === 'track';
|
||||||
const isUnclassified = props.highway === 'unclassified';
|
const isUnclassified = props.highway === 'unclassified';
|
||||||
|
|
||||||
const showAdded = document.getElementById('showAdded').checked;
|
const showAdded = document.getElementById('showAdded').checked;
|
||||||
|
const showConflicted = document.getElementById('showConflicted').checked;
|
||||||
const showRemoved = document.getElementById('showRemoved').checked;
|
const showRemoved = document.getElementById('showRemoved').checked;
|
||||||
const hideService = document.getElementById('hideService').checked;
|
const hideService = document.getElementById('hideService').checked;
|
||||||
const hideUnclassified = document.getElementById('hideUnclassified').checked;
|
const hideUnclassified = document.getElementById('hideUnclassified').checked;
|
||||||
|
|
||||||
// Check removed/added filter
|
// Check removed/added/conflicted filter
|
||||||
if (isRemoved && !showRemoved) return false;
|
if (isRemoved && !showRemoved) return false;
|
||||||
if (!isRemoved && !showAdded) return false;
|
if (isConflicted && !showConflicted) return false;
|
||||||
|
if (!isRemoved && !isConflicted && !showAdded) return false;
|
||||||
|
|
||||||
// Check service/track and unclassified filters
|
// Check service/track and unclassified filters
|
||||||
if (isService && hideService) return false;
|
if (isService && hideService) return false;
|
||||||
@@ -809,7 +816,10 @@ function showMultiFeaturePopup(latlng) {
|
|||||||
if (selectedFeatures.length === 1) {
|
if (selectedFeatures.length === 1) {
|
||||||
// Single selection - show specific status
|
// Single selection - show specific status
|
||||||
const isRemoved = selectedFeatures[0].properties && (selectedFeatures[0].properties.removed === true || selectedFeatures[0].properties.removed === 'True');
|
const isRemoved = selectedFeatures[0].properties && (selectedFeatures[0].properties.removed === true || selectedFeatures[0].properties.removed === 'True');
|
||||||
html += `<div style="margin-top: 8px;"><strong>Status:</strong> ${isRemoved ? 'Removed (Red)' : 'Added (Green)'}</div>`;
|
const isConflicted = !isRemoved && selectedFeatures[0].properties &&
|
||||||
|
(selectedFeatures[0].properties.conflict === true || selectedFeatures[0].properties.conflict === 'True');
|
||||||
|
const statusLabel = isRemoved ? 'Removed (Red)' : (isConflicted ? 'Possibly Conflicted (Yellow)' : 'Added (Green)');
|
||||||
|
html += `<div style="margin-top: 8px;"><strong>Status:</strong> ${statusLabel}</div>`;
|
||||||
if (!isRemoved && osmVertices.length > 0) {
|
if (!isRemoved && osmVertices.length > 0) {
|
||||||
html += connectivitySection(selectedFeatures[0]);
|
html += connectivitySection(selectedFeatures[0]);
|
||||||
}
|
}
|
||||||
@@ -1491,6 +1501,10 @@ function computeRemovalPlan(removedFeat, acceptedAddedFeats, nodeRefCount) {
|
|||||||
const osmNodes = props.osm_nodes;
|
const osmNodes = props.osm_nodes;
|
||||||
const wayCoords = removedFeat.geometry && removedFeat.geometry.coordinates;
|
const wayCoords = removedFeat.geometry && removedFeat.geometry.coordinates;
|
||||||
|
|
||||||
|
if (removedFeat.geometry && removedFeat.geometry.type === 'Point') {
|
||||||
|
return { type: 'deleteNode', osmId, osmVersion, description: `Delete node${osmId ? ' ' + osmId : ''}` };
|
||||||
|
}
|
||||||
|
|
||||||
const tags = {};
|
const tags = {};
|
||||||
Object.entries(props).forEach(([k, v]) => {
|
Object.entries(props).forEach(([k, v]) => {
|
||||||
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
||||||
@@ -1641,22 +1655,29 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newWays = [];
|
const newWays = [];
|
||||||
|
const newNodes = []; // standalone point features (e.g. addresses), not way vertices
|
||||||
const modifiedWays = [];
|
const modifiedWays = [];
|
||||||
const splitTailWays = [];
|
const splitTailWays = [];
|
||||||
const deleteWayIds = []; // [{id, version}]
|
const deleteWayIds = []; // [{id, version}]
|
||||||
const deleteNodeIds = new Map(); // id -> version
|
const deleteNodeIds = new Map(); // id -> version
|
||||||
|
|
||||||
acceptedAdded.forEach(feat => {
|
acceptedAdded.forEach(feat => {
|
||||||
const rawCoords = feat.geometry.coordinates;
|
|
||||||
const midCoords = (feat._snappedCoords || rawCoords).slice(1, -1);
|
|
||||||
const startId = allocEndpointId(rawCoords[0], feat._snapAEnabled !== false ? feat._snapA : null);
|
|
||||||
const endId = allocEndpointId(rawCoords[rawCoords.length - 1], feat._snapBEnabled !== false ? feat._snapB : null);
|
|
||||||
const ndRefs = [startId, ...midCoords.map(c => allocNodeId(c, null)), endId];
|
|
||||||
const tags = {};
|
const tags = {};
|
||||||
Object.entries(feat.properties || {}).forEach(([k, v]) => {
|
Object.entries(feat.properties || {}).forEach(([k, v]) => {
|
||||||
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
||||||
v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (feat.geometry.type === 'Point') {
|
||||||
|
newNodes.push({ id: nextNewId--, coord: feat.geometry.coordinates, tags });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawCoords = feat.geometry.coordinates;
|
||||||
|
const midCoords = (feat._snappedCoords || rawCoords).slice(1, -1);
|
||||||
|
const startId = allocEndpointId(rawCoords[0], feat._snapAEnabled !== false ? feat._snapA : null);
|
||||||
|
const endId = allocEndpointId(rawCoords[rawCoords.length - 1], feat._snapBEnabled !== false ? feat._snapB : null);
|
||||||
|
const ndRefs = [startId, ...midCoords.map(c => allocNodeId(c, null)), endId];
|
||||||
newWays.push({ id: nextNewId--, ndRefs, tags });
|
newWays.push({ id: nextNewId--, ndRefs, tags });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1689,7 +1710,9 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
|||||||
removalPlans.forEach((plan, i) => {
|
removalPlans.forEach((plan, i) => {
|
||||||
if (!checkedPlanIndices.has(i) || !plan.osmId) return;
|
if (!checkedPlanIndices.has(i) || !plan.osmId) return;
|
||||||
|
|
||||||
if (plan.type === 'delete') {
|
if (plan.type === 'deleteNode') {
|
||||||
|
deleteNodeIds.set(plan.osmId, plan.osmVersion || 1);
|
||||||
|
} else if (plan.type === 'delete') {
|
||||||
deleteWayIds.push({ id: plan.osmId, version: plan.osmVersion || 1 });
|
deleteWayIds.push({ id: plan.osmId, version: plan.osmVersion || 1 });
|
||||||
if (plan.deleteNodeIds) plan.deleteNodeIds.forEach(nid => deleteNodeIds.set(nid, osmNodeVersionById.get(nid) || 1));
|
if (plan.deleteNodeIds) plan.deleteNodeIds.forEach(nid => deleteNodeIds.set(nid, osmNodeVersionById.get(nid) || 1));
|
||||||
} else if (plan.type === 'split') {
|
} else if (plan.type === 'split') {
|
||||||
@@ -1730,6 +1753,15 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
|||||||
return x + ' </way>\n';
|
return x + ' </way>\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nodeXml(n) {
|
||||||
|
let x = ` <node id="${n.id}" action="create" lat="${n.coord[1].toFixed(7)}" lon="${n.coord[0].toFixed(7)}"`;
|
||||||
|
const tagEntries = Object.entries(n.tags);
|
||||||
|
if (tagEntries.length === 0) return x + '/>\n';
|
||||||
|
x += '>\n';
|
||||||
|
tagEntries.forEach(([k, v]) => { x += ` <tag k="${esc(k)}" v="${esc(v)}"/>\n`; });
|
||||||
|
return x + ' </node>\n';
|
||||||
|
}
|
||||||
|
|
||||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<osm version="0.6" generator="osm-import-tools">\n\n';
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<osm version="0.6" generator="osm-import-tools">\n\n';
|
||||||
|
|
||||||
referencedExisting.forEach(({ coord, version }, nid) => {
|
referencedExisting.forEach(({ coord, version }, nid) => {
|
||||||
@@ -1740,7 +1772,8 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
|||||||
const [lon, lat] = key.split(',').map(Number);
|
const [lon, lat] = key.split(',').map(Number);
|
||||||
xml += ` <node id="${id}" action="create" lat="${lat.toFixed(7)}" lon="${lon.toFixed(7)}"/>\n`;
|
xml += ` <node id="${id}" action="create" lat="${lat.toFixed(7)}" lon="${lon.toFixed(7)}"/>\n`;
|
||||||
});
|
});
|
||||||
if (referencedExisting.size > 0 || [...nodeRegistry.values()].some(id => id < 0)) xml += '\n';
|
newNodes.forEach(n => { xml += nodeXml(n); });
|
||||||
|
if (referencedExisting.size > 0 || [...nodeRegistry.values()].some(id => id < 0) || newNodes.length > 0) xml += '\n';
|
||||||
|
|
||||||
newWays.forEach(w => { xml += wayXml(w, 'create'); });
|
newWays.forEach(w => { xml += wayXml(w, 'create'); });
|
||||||
modifiedWays.forEach(w => { xml += wayXml(w, 'modify'); });
|
modifiedWays.forEach(w => { xml += wayXml(w, 'modify'); });
|
||||||
@@ -2113,6 +2146,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
createDiffLayer();
|
createDiffLayer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('showConflicted').addEventListener('change', function() {
|
||||||
|
createDiffLayer();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('showRemoved').addEventListener('change', function() {
|
document.getElementById('showRemoved').addEventListener('change', function() {
|
||||||
createDiffLayer();
|
createDiffLayer();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -307,6 +307,10 @@
|
|||||||
<input type="checkbox" id="showAdded" checked>
|
<input type="checkbox" id="showAdded" checked>
|
||||||
Show Added (Green)
|
Show Added (Green)
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="showConflicted" checked>
|
||||||
|
Show Possibly Conflicted (Yellow)
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="showRemoved">
|
<input type="checkbox" id="showRemoved">
|
||||||
Show Removed (Red)
|
Show Removed (Red)
|
||||||
|
|||||||
Reference in New Issue
Block a user