Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
* Make tags like `contact:instagram` clickable if they contain a plain username, or a full URL ([#12306], thanks [@k-yle])
* Show suggestions in combobox dropdown also when the entered text contains a few typos ([#8802])
* Render the `side` arrow of cyclist waiting aid features ([#12374], thanks [@RudyTheDev])
* Render diameter and radius tags when a node is selected ([#9732], thanks [@k-yle])
#### :scissors: Operations
#### :camera: Street-Level
* Add high-resolution toggle for Mapilio photo viewer ([#12353], thanks [@sezerbozbiyik])
Expand All @@ -69,6 +70,7 @@ _Breaking developer changes, which may affect downstream projects or sites that

[#8802]: https://github.com/openstreetmap/iD/issues/8802
[#8848]: https://github.com/openstreetmap/iD/issues/8848
[#9732]: https://github.com/openstreetmap/iD/pull/9732
[#10615]: https://github.com/openstreetmap/iD/pull/10615
[#11634]: https://github.com/openstreetmap/iD/pull/11634
[#12297]: https://github.com/openstreetmap/iD/issues/12297
Expand Down
19 changes: 19 additions & 0 deletions css/20_map.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ g.vertex.selected .shadow {
fill-opacity: 0.7;
}

/* circle to visualize radius/diameter */
g.point .radius,
g.vertex .radius {
fill: #333;
Comment thread
tordans marked this conversation as resolved.
fill-opacity: 0.6;
stroke: #fff;
stroke-width: 0.5px;
stroke-opacity: 0.75;
}

.fill-wireframe g.point .radius,
.fill-wireframe g.vertex .radius {
fill: none;
}
.surface.tr g.point .radius,
.surface.tr g.vertex .radius {
display: none;
}


/* lines */
.preset-icon .icon.iD-other-line {
Expand Down
1 change: 1 addition & 0 deletions modules/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { coreGraph } from './graph';
export { coreHistory } from './history';
export { coreLocalizer, t, localizer } from './localizer';
export { locationManager } from './location_manager';
export * from './planar';
export { prefs } from './preferences';
export { coreTree } from './tree';
export { coreUploader } from './uploader';
Expand Down
113 changes: 113 additions & 0 deletions modules/core/planar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { GeoProjection } from 'd3';
import { geoMetersToLat } from '../geo';

/**
* Parses a distance value from an OSM tag, which
* could either be a number, or a number with a unit
* (e.g. `15 m`).
* @returns value in metres or `undefined` if the value is invalid
*/
export function parseDistanceWithUnit(tagValue: string, defaultUnit: string) {
if (!tagValue) return undefined; // tag doesn't exist

// special case when feet and inches are used together.
const imperialCombo = tagValue.match(
/([\d.]+) *('|ft|foot|feet) *([\d.]+) *("|in|inch|inches)/,
);
if (imperialCombo) {
const feet = +imperialCombo[1];
const inches = +imperialCombo[3];
return feet / 3.281 + inches / 39.37;
}

// the remaining code parses normal values (number + optional unit)

const unit = tagValue.match(/[^\d.]+/)?.[0].trim() || defaultUnit;
const value = parseFloat(tagValue);

if (Number.isNaN(value) || !Number.isFinite(value)) return undefined; // invalid value

// convert to metres
switch (unit) {
case 'mm':
return value / 1e3;
case 'cm':
return value / 1e2;
case 'metres':
case 'm':
return value;
case 'hectometres':
case 'hm':
return value * 1e2;
case 'kilometres':
case 'km':
return value * 1e3;

case 'statute_miles':
case 'miles':
case 'mi':
return value * 1609;

case 'nm':
return value * 1852;

case 'yard':
case 'yards':
case 'yd':
return value / 1.094;

case '\'':
case 'feet':
case 'foot':
case 'ft':
return value / 3.281;

case '"':
case 'inch':
case 'inches':
case 'in':
return value / 39.37;

default:
return undefined;
}
}

export function getRadiusTag(tags: Tags) {
const diameter =
parseDistanceWithUnit(tags.diameter, 'mm') ||
parseDistanceWithUnit(tags.diameter_crown, 'm') ||
parseDistanceWithUnit(tags['hole:diameter'], 'm');
if (diameter) return diameter / 2;

const radius =
parseDistanceWithUnit(tags.radius, 'm') ||
parseDistanceWithUnit(tags.crown_radius, 'm') ||
parseDistanceWithUnit(
tags['seamark:anchor_berth:radius'],
tags['seamark:anchor_berth:units'] || 'm',
);
if (radius) return radius;

return undefined;
}

export function getRadiusInPixels(node: iD.OsmNode, projection: GeoProjection) {
const radius = getRadiusTag(node.tags);
if (!radius) return 0;

const center = projection(node.loc)!;
const pointOnCircumference = projection([
node.loc[0],
node.loc[1] + geoMetersToLat(radius),
])!;

// The radius is the difference in latitude between
// the centre and the point on the circumference.
const pixels = center[1] - pointOnCircumference[1];

// don't try to render a circle that's too big to fit on the screen
if (pixels > window.innerHeight || pixels > window.innerWidth) return 0;

return Math.max(0, pixels);
}
1 change: 1 addition & 0 deletions modules/renderer/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export function rendererMap(context) {
if (mode && mode.id === 'select') {
// update selected vertices - the user might have just double-clicked a way,
// creating a new vertex, triggering a partial redraw without a mode change
surface.call(drawPoints.drawSelected, graph, map.extent());
surface.call(drawVertices.drawSelected, graph, map.extent());
}

Expand Down
23 changes: 23 additions & 0 deletions modules/svg/points.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { osmEntity } from '../osm';
import { svgPointTransform } from './helpers';
import { svgTagClasses } from './tag_classes';
import { presetManager } from '../presets';
import { getRadiusInPixels, getRadiusTag } from '../core';
import { textWidth, isAddressPoint } from './labels';

export function svgPoints(projection, context) {
Expand Down Expand Up @@ -87,6 +88,7 @@ export function svgPoints(projection, context) {


function drawPoints(selection, graph, entities, filter) {
const selected = context.selectedIDs();
var wireframe = context.surface().classed('fill-wireframe');
var zoom = geoScaleToZoom(projection.scale());
var base = context.history().base();
Expand Down Expand Up @@ -122,6 +124,20 @@ export function svgPoints(projection, context) {
.append('path')
.call(markerPath, 'shadow');

// Highlighted vertices with a radius/diameter get a circle
const circles = groups
.selectAll('.radius')
.data(d => selected.includes(d.id) && getRadiusTag(d.tags) ? [d] : [], fastEntityKey);

circles.exit().remove();
circles.enter()
.append('circle')
.attr('class', 'radius')
.merge(circles)
.attr('r', node =>
Math.max(0, getRadiusInPixels(node, context.projection))
);

enter.each(function(d) {
if (isAddressPoint(d.tags)) return;
d3_select(this)
Expand Down Expand Up @@ -173,6 +189,13 @@ export function svgPoints(projection, context) {
.call(drawTargets, graph, points, filter);
}

drawPoints.drawSelected = (selection, graph, extent) => {
const selectedIds = context.selectedIDs();
const filter = (d) => selectedIds.includes(d.id);
const seletcedEntities = selectedIds.map(id => graph.hasEntity(id)).filter(Boolean);
drawPoints(selection, graph, seletcedEntities, filter, extent, false);
};


return drawPoints;
}
15 changes: 15 additions & 0 deletions modules/svg/vertices.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { presetManager } from '../presets';
import { geoScaleToZoom } from '../geo';
import { osmEntity } from '../osm';
import { svgPassiveVertex, svgPointTransform } from './helpers';
import { getRadiusInPixels, getRadiusTag } from '../core';
import { svgTagClasses } from './tag_classes';

export function svgVertices(projection, context) {
Expand Down Expand Up @@ -165,6 +166,20 @@ export function svgVertices(projection, context) {
return picon ? '#' + picon : '';
});

// Highlighted vertices with a radius/diameter get a circle
const circles = groups
.selectAll('.radius')
.data(d => context.selectedIDs().includes(d.id) && getRadiusTag(d.tags) ? [d] : [], fastEntityKey);

circles.exit().remove();
circles.enter()
.append('circle')
.attr('class', 'radius')
.merge(circles)
.attr('r', node =>
Math.max(0, getRadiusInPixels(node, context.projection))
);


// Vertices with directions get viewfields
var dgroups = groups
Expand Down
74 changes: 74 additions & 0 deletions test/spec/core/planar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { GeoProjection } from 'd3';
import type { Vec2 } from '../../../modules/geo/vector';

describe(iD.parseDistanceWithUnit, () => {
it('handles valid tag values', () => {
expect(iD.parseDistanceWithUnit(' 12.5 ', 'm')).toBe(12.5);
expect(iD.parseDistanceWithUnit('12cm', 'm')).toBe(0.12);
expect(iD.parseDistanceWithUnit('12 cm', 'm')).toBe(0.12);
expect(iD.parseDistanceWithUnit('12000', 'mm')).toBe(12);
expect(iD.parseDistanceWithUnit('5feet', 'm')).toBe(1.5239256324291375);
expect(iD.parseDistanceWithUnit('5ft', 'm')).toBe(1.5239256324291375);
expect(iD.parseDistanceWithUnit('5\'', 'm')).toBe(1.5239256324291375);
expect(iD.parseDistanceWithUnit('5\'9"', 'm')).toBe(1.7525260896300519);
expect(iD.parseDistanceWithUnit('5foot 9"', 'm')).toBe(
1.7525260896300519,
);
expect(iD.parseDistanceWithUnit(' 5 feet 9 inches ', 'm')).toBe(
1.7525260896300519,
);
expect(iD.parseDistanceWithUnit(' 5.5\' 9.12" ', 'm')).toBe(
1.9079666589689779,
);
});

it('handles invalid tag values', () => {
expect(iD.parseDistanceWithUnit('', 'm')).toBeUndefined();
expect(iD.parseDistanceWithUnit('15 bananas', 'm')).toBeUndefined();
expect(iD.parseDistanceWithUnit('qwertyuiop', 'm')).toBeUndefined();
});
});

describe(iD.getRadiusTag, () => {
it('can identify the radius tag', () => {
expect(iD.getRadiusTag({ radius: '10' })).toBe(10);
expect(iD.getRadiusTag({ 'hole:diameter': '10' })).toBe(5);
expect(iD.getRadiusTag({ 'hole:diameter': '6 ft' })).toBe(
0.9143553794574825,
);
expect(iD.getRadiusTag({ 'seamark:anchor_berth:radius': '0.2' })).toBe(
0.2,
);
expect(
iD.getRadiusTag({
'seamark:anchor_berth:radius': '0.2',
'seamark:anchor_berth:units': 'nm',
}),
).toBe(370.40000000000003);

expect(iD.getRadiusTag({})).toBeUndefined();
});
});

describe(iD.getRadiusInPixels, () => {
it('sanity check that the test environment has screen dimensions', () => {
expect(window.innerWidth).toBe(1024);
expect(window.innerHeight).toBe(768);
});
it('can convert metres to pixels', () => {
// fake coordinate system
const projection = <GeoProjection>(
(([lng, lat]) => [lng / 4 - 5, lat / 2 - 3])
);
projection.invert = ([x, y]: Vec2) => [(x + 5) * 4, (y + 3) * 2];

const node = iD.osmNode({
id: 'a',
loc: [174.7822771, -36.809856],
tags: { highway: 'turning_circle', diameter: '16m' },
});

const px = 0.0002404180484063545;
expect(iD.getRadiusInPixels(node, d3.geoMercator())).toBe(px);
});
});
Loading